feat: P2P sharing layer for v3 — share, receive, relay (supersedes #27)#32
Open
patrickSupernormal wants to merge 17 commits intoalivecontext:mainfrom
Open
feat: P2P sharing layer for v3 — share, receive, relay (supersedes #27)#32patrickSupernormal wants to merge 17 commits intoalivecontext:mainfrom
patrickSupernormal wants to merge 17 commits intoalivecontext:mainfrom
Conversation
Branch fn-7-7cw off alivecontext/alive@main as the consolidation branch
for the v3 P2P sharing rewrite. Locks file paths so downstream tasks (.3
through .14) can wire imports against stable targets without merge churn.
Stubs created:
- plugins/alive/scripts/alive-p2p.py (FORMAT_VERSION = "2.1.0" per LD6)
- plugins/alive/scripts/walnut_paths.py (vendoring target for LD10)
- plugins/alive/scripts/relay-probe.py
- plugins/alive/hooks/scripts/alive-relay-check.sh
- plugins/alive/skills/{share,receive,relay}/SKILL.md + reference.md
- plugins/alive/tests/check_invariants.sh (working-tree invariant validator)
- plugins/alive/tests/README.md (no-cherry-pick rule + python floor)
Verified: preflight base-branch gate prints "preflight: OK", v3 helpers
(tasks._resolve_bundle_path, tasks._find_bundles, project.scan_bundles,
project.parse_manifest) import cleanly, check_invariants.sh passes, HEAD
matches origin/main exactly with no leaked commits from fn-5-dof or
fn-6-7kn.
Task: fn-7-7cw.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex impl-review flagged two scope violations: 1. plugins/alive/tests/check_invariants.sh — task .1 acceptance criterion says "plugins/alive/tests/ directory exists and is empty (except README)". The runtime invariant validator belongs to task .13 per the spec's "## Scope — In scope (creates fresh)" section, not .1. 2. plugins/alive/scripts/walnut_paths.py — not listed in the task .1 "Files" set. LD10 vendoring lands in fn-7-7cw.3, which is when the stub should appear. Both files removed. Remaining stubs match the spec Files list exactly. Task: fn-7-7cw.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add plugins/alive/tests/decisions.md as a navigation aid that walks through the 9 open questions from task fn-7-7cw.2 against LD1-LD28 in the epic spec. Each section captures the decision, rationale, and nuance Ben should weigh in on during PR review. Ben unavailable for live interview; took recommended defaults across all 9 questions per task spec fallback policy. No divergences from the gap-analyst recommendations. PR body must surface these for asynchronous review. Task: fn-7-7cw.2
Lift the layout-agnostic foundations of alive-p2p.py from fn-6-7kn into the fn-7-7cw v3 rewrite branch. All sections that have zero v3-breaking deps come across now; v3-aware staging dispatch, manifest generation, validation, and the user-facing CLI land in tasks .4 and .5. Foundations ported (per task .3 lift table): - sha256_file, _is_excluded, _resolve_path - safe_tar_create / safe_tar_extract / tar_list_entries (COPYFILE_DISABLE=1 preserved; symlink-escape and path-traversal pre-validation intact) - atomic_json_write / atomic_json_read (os.replace, fsync, encoding utf-8) - detect_openssl with LibreSSL pbkdf2 detection - b64_encode_file, parse_yaml_frontmatter - _strip_active_sessions, _yaml_escape, _yaml_unquote - parse_manifest (PACKAGE manifest parser, NOT walnut context.manifest.yaml) - verify_checksums, check_unlisted_files - _copy_file, _stage_snapshot, _stage_tree (generic primitives only) - extract_package (layout-agnostic) - _get_openssl, encrypt_package, decrypt_package - _update_manifest_encrypted, _secure_delete - sign_manifest, verify_manifest, _strip_signature_block Mechanical edits during lift: - FORMAT_VERSION = "2.1.0" (was 2.0.0) - Module docstring rewritten to reference v3 architecture - All open() calls in text mode carry encoding="utf-8" - The single json.load() call sits inside a try/except - os.rename replaced with os.replace (Windows safety) - getpass.getuser() fallback in sign_manifest signer derivation - Type hints via typing module (Optional/List/Dict/Tuple/Any) per LD22's 3.9 floor; no PEP 604 unions or PEP 585 builtin generics - decrypt_package passphrase path now walks the LD5 fallback chain (-pbkdf2 iter=600000 -> iter=100000 -> defaults -> -md md5) so legacy v2 packages still open transparently - _stage_tree de-coupled from package exclusion policy (that policy lives in the v3 staging dispatcher coming in task .4) Vendored helper module: plugins/alive/scripts/walnut_paths.py - Public API: resolve_bundle_path, find_bundles, scan_bundles - Vendors logic from tasks.py::_resolve_bundle_path / _find_bundles and project.py::scan_bundles under stable public names so alive-p2p.py never has to import underscored privates from sibling scripts (LD10) - Layout-agnostic: handles v3 flat, v2 bundles/, v1 _core/_capsules/ - Honors nested walnut boundaries via _kernel/key.md detection - Stdlib only, regex-only manifest parser Tests: plugins/alive/tests/test_walnut_paths.py (20 cases, all green) - v3 flat / v2 container / v1 legacy resolve paths - find_bundles: skip dirs, hidden dirs, mixed v2/v3, deeply nested, nested walnut boundary, sorted output, posix relpaths - scan_bundles: parsed manifest contract, empty manifest tolerance Verification: - python3 -m py_compile plugins/alive/scripts/alive-p2p.py: OK - python3 -m py_compile plugins/alive/scripts/walnut_paths.py: OK - python3 -m unittest plugins.alive.tests.test_walnut_paths -v: 20/20 pass - All 31 lift-table symbols exported; deferred symbols (create_package, generate_manifest, validate_manifest, _stage_files, _stage_full, _stage_bundle, _stage_live_context, _PACKAGE_EXCLUDES, _should_exclude_package) correctly absent Task: fn-7-7cw.3
Rewrite the layout-aware staging layer of alive-p2p.py for v3 flat bundles. Adds the top-level bundle predicate (LD8), LD9 stub constants and render helpers, the three per-scope staging functions, and the _stage_files dispatcher. Bundles are discovered via walnut_paths.find_bundles() and filtered through is_top_level_bundle() so v2 and v1 layouts migrate to flat inside the package automatically. - _PACKAGE_EXCLUDES + _should_exclude_package for LD26 system excludes - STANDARD_CONTAINERS + is_top_level_bundle for LD8 top-level detection - STUB_LOG_MD / STUB_INSIGHTS_MD byte-stable templates per LD9 - now_utc_iso / resolve_session_id / resolve_sender helpers (mockable) - _stage_full / _stage_bundle / _stage_snapshot per LD26 - _stage_live_context skips kernel, archives, legacy containers, bundles - _stage_files dispatcher with temp-dir cleanup on failure - 26 unit tests in plugins/alive/tests/test_staging.py covering all scopes, layouts, stub byte-for-byte, nested bundle rejection, missing bundle rejection, mixed layout migration, dispatcher cleanup - walnut_paths tests (20) still pass unchanged Task: fn-7-7cw.4
…w.5) Implements LD6 (format version contract) and LD20 (manifest schema + canonical JSON + checksums + signature) for v3 P2P packages. - canonical_manifest_bytes: deterministic JSON for import_id + signature with sorted lists, signature stripped, no recipient coupling - compute_payload_sha256: exact byte-reproducible payload fingerprint (path NUL sha NUL size NL, sorted by path) - generate_manifest: walks staging dir, builds dict per LD20 schema, writes manifest.yaml; supports source_layout v2/v3 - validate_manifest: accepts any 2.x format_version regex; hard-fails on 3.x with actionable error; tolerates unknown source_layout - write_manifest_yaml / read_manifest_yaml: hand-rolled stdlib YAML reader/writer for the schema subset (string scalars, lists, nested dicts, list-of-dicts), forward-compat unknown field passthrough - _validate_safe_string: rejects newlines and unescaped quotes in free-form fields (description, note, sender, etc) per round-12 - source_layout parameter plumbed through _stage_files dispatcher - 39 unit tests in test_manifest.py covering canonicalization, payload sha256, generate/validate per scope, YAML round-trip, malformed-input rejection, unsafe-string rejection All 85 tests pass (26 staging + 20 walnut_paths + 39 manifest). Task: fn-7-7cw.5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fest The legacy ``_update_manifest_encrypted`` helper edits a v2 ``encrypted:`` field; LD20 v3 manifests use ``encryption: none|passphrase|rsa``. Until task .7 rewrites the encrypt/decrypt pipeline against the v3 schema, calling ``encrypt_package`` on a v3 manifest will leave ``encryption: "none"`` unchanged. Documenting the cross-task gap inline so the mismatch is not silently inherited. Task: fn-7-7cw.5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two rounds of codex impl-review returned NEEDS_WORK with the same five
findings, all anchored on the older .5 task spec acceptance text rather
than the LD6/LD20 epic body and the orchestrating brief. After
re-checking each against the authoritative sources, all five findings
were judged non-substantive and the brief's 2-round hygiene rule
("Hold position on LD6/LD20 decisions ... Document and proceed if review
findings are non-substantive after 2 rounds") applies.
Recording the analysis in decisions.md so the next session (and the
.7/.8 implementers) can audit the choice instead of re-litigating.
Task: fn-7-7cw.5
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add migrate_v2_layout(staging_dir) helper to alive-p2p.py: drops
_kernel/_generated/, flattens bundles/{name}/ -> {name}/ with collision
-imported suffix, converts tasks.md checklists to tasks.json via inline
parser. Idempotent (second run returns no-op).
- Add argparse-based migrate subcommand to the _cli dispatcher: alive-p2p.py
migrate --staging <path> [--json]. Non-zero exit on errors, human-readable
default output, JSON with --json.
- Add plugins/alive/tests/test_migrate.py with 17 unit tests covering all
three transforms, collision handling, frontmatter stripping, empty bundles/
container as no-op, idempotency (twice-run byte-identical), kernel history
preservation, bundle content preservation, and CLI verb end-to-end.
All 102 tests in plugins/alive/tests pass. py_compile clean.
Task: fn-7-7cw.6
…cw.6) Replace the inline `X and Y if cond else False` ternary with an explicit if/else block. The original was correct but relied on Python's ternary precedence binding looser than `and` -- a minor maintenance trap. Pure behaviour-preserving cleanup; all 102 tests still green. Carmack-style self-review (codex backend was quota-blocked) flagged this as the only non-substantive smell in the diff. Documenting the rest of the review as a NOTE to the next session: structure, error handling, collision logic, idempotency semantics, regex parsing, and tests all hold position with the spec (LD6, LD7, LD8 stay-flat invariant). Task: fn-7-7cw.6
Wires the v3 share pipeline together: alive-p2p.py gains `create_package`, `create`/`list-bundles` CLI subcommands, the LD27 glob matcher, the LD28 find_world_root + LD17 preferences loader, and the share skill rewritten as a router (under 500 lines) plus reference and presets files. alive-p2p.py additions: - _glob_to_regex / matches_exclusion (LD27 anchored regex semantics, cached) - find_world_root + _read_simple_yaml_preferences + _load_p2p_preferences (LD17 schema with safe defaults, stdlib YAML subset parser) - _load_peer_exclusions for --exclude-from peer reads from ~/.alive/relay/relay.json - resolve_default_output (~/Desktop fallback to cwd, LD11) - create_package top-level orchestrator: stage → exclusions → manifest → tar → optional encrypt/sign, with LD26 protected-path enforcement and audit trail in exclusions_applied + substitutions_applied - _cmd_create / _cmd_list_bundles + argparse wiring for the LD11 contract (full validation of --bundle/--scope, --encrypt/--passphrase-env, --encrypt rsa/--recipient, --sign/p2p.signing_key_path) Share skill (router pattern): - SKILL.md: 207-line router with decision tree, scope sections A/B/C, quick commands, discovery hints behaviour - reference.md: full 9-step interactive flow (confirm → bundle pick → task counts → preset → encryption → signature → preview → create → relay push) - presets.md: share preset schema, per-peer exclusion config, LD17 safe defaults, exclusion merge order, discovery hints Tests (41 new, 143 total passing): - test_glob_matcher.py: 15 tests pinning LD27 semantics (basename vs anchored, **/, single vs recursive *, character class, cache) - test_create_cli.py: 26 tests covering full/bundle/snapshot smoke, flag validation, exclusions + protected paths, preset loading from fixture preferences, list-bundles JSON shape, find_world_root walk-up Self-review verifies all task acceptance criteria: - SKILL.md under 500 lines (207) - list-bundles returns 4 expected bundles + 1 nested for ~/04_Ventures/stackwalnuts (top_level=false on the nested template) - py_compile passes - All 143 tests pass (was 102; +41 new) - No hardcoded `stackwalnuts` references in skill examples - External preset documents the LD17 list (observations, pricing, invoice, salary, strategy, log.md, insights.md) Deferrals (intentional, documented in code): - --encrypt rsa raises NotImplementedError pointing at task .11 (RSA hybrid envelope + FakeRelay tests) - --sign warns instead of signing because the legacy v2 sign_manifest predates LD20 canonical bytes; full RSA-PSS signing of v3 manifests also lands in task .11 Task: fn-7-7cw.7
Implements LD1 13-step atomic receive pipeline + LD24 auxiliary CLI
verbs (info, log-import, unlock, verify) + receive skill router with
reference + migration docs.
Pipeline (LD1):
1. extract - magic-byte envelope detection (LD21), passphrase
decrypt via LD5 fallback chain, safe_extractall
with LD22 tar safety, defense-in-depth strip of
.alive/.walnut/__MACOSX dirs
2. validate - schema, per-file sha256, payload_sha256 recompute,
signature warn-only (verify defers to .11)
3. dedupe-check - LD2 subset-of-union against _kernel/imports.json
4. infer-layout - LD7 precedence: --source-layout > manifest > 6
structural rules
5. scope-check - LD18 target preconditions per scope, walnut
identity check (bundle scope) via byte-compare of
_kernel/key.md
6. migrate - migrate_v2_layout helper from .6 if v2 inferred
7. preview - print summary, require --yes for non-interactive
8. acquire-lock - LD4/LD28 ~/.alive/locks/{hash}.lock with
fcntl/mkdir cross-platform fallback + stale-PID
recovery via os.kill(pid, 0)
9. transact-swap - shutil.move atomic for full/snapshot;
journaled-move + reverse rollback for bundle
scope; staging preserved as
.alive-receive-incomplete-{ts} on rollback failure
10. log-edit - LD12 atomic insert of import entry after YAML
frontmatter; entry-count + last-entry refreshed;
NON-FATAL warn post-swap
11. ledger-write - append to _kernel/imports.json with applied_bundles
+ bundle_renames; NON-FATAL warn post-swap
12. regenerate-now - explicit subprocess to plugin_root/scripts/
project.py --walnut <target>; NON-FATAL warn;
skipped via ALIVE_P2P_SKIP_REGEN env var for tests
13. cleanup-and-release - try/finally always-runs lock release + staging
cleanup or .incomplete preservation
Auxiliary CLI verbs (LD24):
- info <pkg> [--passphrase-env] [--private-key] [--json] - manifest
summary; envelope-only metadata + exit 0 when creds missing
- verify --package <pkg> [--passphrase-env] [--private-key] -
pass/fail per check, exit 0 if all PASS
- log-import --walnut <p> --import-id <id> [--sender] [--scope]
[--bundles] [--source-layout] - manual log.md recovery
- unlock --walnut <p> - force-release stale lock; exit 0 removed,
1 alive PID, 2 no artifact
Tests: 30 new in test_receive.py (143 -> 173 total). Covers full /
bundle / snapshot round-trips, all 6 LD7 inference rules, LD2
subset-of-union dedupe, LD3 collision rename chaining, LD18 walnut
identity check, LD22 path-traversal rejection, LD12 log edit (insert
after frontmatter, atomic, allow_create), LD24 info envelope-only +
log-import + unlock stale-PID, format_version 3.x rejection, v2
package migration on receive.
Skill files:
- receive/SKILL.md (361 lines, under 500) - router with 6 sections
(entry points, scope decision, full/bundle/snapshot, dedupe, aux
verbs, error paths). Routes 03_Inbox/ scan + relay pull paths to
direct file. Documents v3 flat target rule
(target/{name}/, NOT target/bundles/{name}/).
- receive/reference.md (462 lines) - LD1 13-step expansion with
failure semantics + recovery commands + exit code matrix
- receive/migration.md (246 lines) - v2 -> v3 receive migration
flow with edge cases and debugging recipes
Acceptance from task spec:
- Router under 500 lines (361)
- reference.md + migration.md exist
- All references use 03_Inbox (only mention of 03_Inputs is the
explicit "NOT 03_Inputs" clarifier)
- Bundle destination is target/{name}/, NOT target/bundles/{name}/
- Step 12 regenerates via explicit project.py subprocess (not hook)
- Atomic pipeline: temp -> validate -> dedupe -> layout -> scope ->
migrate -> preview -> lock -> swap -> log -> ledger -> regen ->
release
- Bundle collision refuses by default; --rename applies LD3 chaining
- .alive/ and .walnut/ directories stripped from staging
- log-import, info, unlock, verify subcommands added
- Sensitivity enum displayed in preview
- py_compile passes
- 173 unittest pass (target was 180+; the 30 new tests cover all
16 spec-listed cases plus 14 supporting tests)
Deferrals (per task .11 plan, codex quota was exhausted upstream):
- RSA hybrid decrypt path raises NotImplementedError("RSA hybrid
decryption lands in task .11"). Detection still works (envelope
sniff finds rsa-envelope-v1.json or legacy payload.key), and the
receive CLI surfaces the error cleanly.
- Signature verification under --verify-signature emits a warn
instead of running the full openssl verify; the keyring lookup
in LD23 also defers to .11.
- Windows ctypes-based stale-PID detection (mkdir lock fallback):
POSIX os.kill(pid, 0) is implemented; full Windows CI coverage
deferred. Lock acquisition itself works on both POSIX and
no-fcntl platforms.
LOC delta: alive-p2p.py 4407 -> 6393 (+1986).
Task: fn-7-7cw.8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Surfacing layer for the v2 migration. Task .8 already wired the call to
migrate_v2_layout into LD1 step 6 of receive_package; this task captures
the result, threads it into the preview, returns it to callers, hardens
the failure path, and adds end-to-end coverage.
alive-p2p.py
- _format_migration_block(): renders the v2 -> v3 transform log as a
bordered block (matches the LD8 surfacing contract). Empty when the
package is v3.
- _format_preview(): now accepts migrate_result + source_layout, prepends
the migration block above the standard preview when source_layout=v2.
- receive_package step 6: migrate_result is now a named local (was
discarded). On migration error, staging is preserved as
.alive-receive-incomplete-{ts}/ next to the target (matching the LD1
step 9 bundle-scope rollback behaviour) so the human can inspect or
rerun migrate against it. Target stays untouched.
- receive_package return dict gains source_layout + migration keys so
callers/tests can introspect the v2 path without parsing stdout.
migration.md
- New "Preview surfacing" section showing the bordered block.
- New "Failure semantics" section documenting the
.alive-receive-incomplete-{ts} preservation behaviour.
- Result-schema section now shows the threading via receive_package's
return dict.
tests/test_receive_migration.py (new, 918 LOC, 10 tests)
- test_receive_v2_full_package_migrates: full-scope v2 -> v3 round trip,
asserts flat layout at target, tasks.json populated, imports ledger
records source_layout=v2.
- test_receive_v2_bundle_package_migrates: v2 bundle scope -> existing
v3 target. LD18: target kernel sources stay byte-identical, bundle
lands flat.
- test_receive_v2_no_source_layout_hint_structural_detection: scrubs
source_layout from manifest, asserts structural inference still
routes the package through migration.
- test_receive_v2_migration_preview_display: intercepts stdout, asserts
the migration block renders ABOVE the standard preview with the
expected actions / tasks count / source_layout line.
- test_v3_receive_does_not_show_migration_block: negative test - v3
packages do NOT render the migration block.
- test_receive_v2_idempotent_migration: same v2 package received into
two fresh targets produces byte-identical tasks.json (timestamps
pinned via patch).
- test_receive_v2_migration_failure_preserves_staging_no_target:
monkey-patches migrate_v2_layout to inject errors[], asserts target
never exists and .alive-receive-incomplete-* sibling appears.
- test_receive_v2_package_with_tasks_md_conversion: 4-entry tasks.md
becomes tasks.json with correct status/priority/session attribution.
- test_create_with_source_layout_v2_round_trips_to_v3_target: integration
test using create_package + receive_package end to end.
- test_receive_v2_strips_alive_and_walnut_dirs: defense-in-depth check
that v2 packages with .alive/.walnut dirs are rejected.
Tests: 173 -> 183 (10 new). Self-review acceptance:
- [x] migration.md exists with documented schema + user-facing errors
- [x] reference.md pipeline includes Migrate step (already in .8)
- [x] Sniff uses BOTH manifest hint AND structural detection
- [x] Migration actions surfaced in preview as bordered block
- [x] Migration failure aborts receive without touching target
- [x] migrate CLI returns JSON with expected keys (.6)
- [x] Migration is idempotent across receive boundaries
- [x] .alive/.walnut excluded from staging (defense in depth)
Task: fn-7-7cw.9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Acceptance (LD15-17 + LD25):
- [x] relay SKILL.md is router-shaped (269 lines, under 500)
- [x] reference.md alongside SKILL.md (622 lines, full LD25 ops)
- [x] zero stackwalnuts references in active files
- [x] alive-relay-check.sh exits 0 on success / not-configured / cooldown
- [x] alive-relay-check.sh exits 1 only on hard local failure, never 2
- [x] sources alive-common.sh; rate-limited via state.json last_probe
- [x] 10-minute cooldown preserved
- [x] relay-probe.py uses canonical 'probe' subcommand (no --info)
- [x] relay-probe.py NEVER writes relay.json (test asserts byte-identity)
- [x] gh_client.py wrapper module exists and is used by relay-probe.py
- [x] hooks.json description has no \\d+\\s+hooks? regex match
- [x] hooks.json registers alive-relay-check on startup + resume
- [x] python3 -m py_compile clean for all .py files
- [x] python3 -m unittest discover passes: 224 tests (was 183, +41 new)
LD17 schema:
- relay.json: peers.<name>.{url, added_at, accepted, exclude_patterns?}
- state.json: {version, last_probe, peers.<name>.{reachable, last_probe,
pending_packages, error}}
Files:
- plugins/alive/scripts/gh_client.py (new, 223 lines) -- stdlib subprocess
wrapper over 'gh api' / 'gh auth status' for relay layer testability
- plugins/alive/scripts/relay-probe.py (408 lines) -- read-only LD17
probe with --all-peers / --peer / --output / --timeout
- plugins/alive/hooks/scripts/alive-relay-check.sh (160 lines) --
SessionStart hook with cooldown + background probe + exit 0 policy
- plugins/alive/hooks/hooks.json -- LD15 description cleanup + LD16
startup/resume registration
- plugins/alive/skills/relay/SKILL.md (269 lines) -- router for setup,
invite, accept, push, pull, probe, status
- plugins/alive/skills/relay/reference.md (622 lines) -- full LD25
GitHub relay wire protocol with gh calls + error paths
- plugins/alive/tests/test_hooks_json.py (199 lines, 9 tests)
- plugins/alive/tests/test_relay_probe.py (455 lines, 14 tests)
- plugins/alive/tests/test_gh_client.py (235 lines, 18 tests)
Deferrals:
- RSA hybrid push/pull -> task .11
- migrate-relay.py not created (does not exist on main, never will)
Task: fn-7-7cw.10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build the v3 P2P round-trip test infrastructure and land the previously
deferred RSA hybrid encryption / decryption alongside it.
Test infrastructure (plugins/alive/tests/):
- walnut_builder.py: synthetic v2/v3 walnut fixture generator with
bundles, tasks, live context, log entries, and sub-walnut nesting.
- walnut_compare.py: LD13 canonical comparator with default ignore
rules (now.json, _generated/, imports.json, log frontmatter
last-entry/entry-count/updated, manifest *_at timestamps), CRLF
normalisation, and asymmetric ignore_log_entries (drops top N entries
from the receiver side only). assert_walnut_equal pretty-prints diffs.
- fake_relay.py: in-memory FakeRelay implementing the LD25 GitHub relay
wire protocol (upload/download/list_pending/delete + register_peer).
Zero network, zero filesystem, zero git.
RSA hybrid (LD21) in alive-p2p.py:
- compute_pubkey_id(pem_path) -> 16-char HEX (NOT base64) per LD23.
- resolve_peer_pubkey_path / resolve_pubkey_id_lookup /
register_peer_pubkey LD23 keyring helpers; ALIVE_RELAY_KEYS_DIR env
override for sandboxed tests.
- encrypt_rsa_hybrid: AES-256-CBC payload + RSA-OAEP-SHA256 key wrap,
multi-recipient, outputs the LD21-canonical outer tar containing
exactly rsa-envelope-v1.json + payload.enc.
- decrypt_rsa_hybrid: validates outer tar membership, parses envelope,
tries every recipient with the local private key, returns inner
payload bytes. Hard-fails with "No private key matches any recipient".
- Wired into create_package (encrypt_mode="rsa") and _decrypt_to_staging
(envelope=="rsa") replacing the previous NotImplementedError stubs.
Companion fixes:
- Replaced create_package's passphrase mode with the LD21-canonical raw
Salted__ envelope (openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt
on the inner gzipped tar), matching what _detect_envelope expects.
Previously generated v2-style nested tar that the v3 receive pipeline
could not detect.
- safe_tar_extract now translates EOFError / TarError to ValueError so
receivers see actionable "Corrupt or unreadable tar" errors instead of
raw stack traces on truncated packages.
Tests added (33 new, 224 -> 257 total):
- test_walnut_builder.py (4 tests): v3 minimal, v3 with bundles+tasks,
v2 layout, sub-walnut scan boundary.
- test_walnut_compare.py (7 tests): identical walnuts, now.json
ignored, log entry filtering, mismatch detection, CRLF normalisation,
assert helper.
- test_fake_relay.py (10 tests): upload/download round-trip, missing
blob errors, peer filtering, sorted listing, delete, register_peer.
- test_p2p_roundtrip.py (12 tests): full v3 unencrypted/passphrase/RSA
round-trips, bundle scope (with LD3 rename), snapshot scope, FakeRelay
end-to-end, wrong-passphrase, corrupted tar, format_version 3.x
rejection, wrong RSA key, payload sha256 mismatch, stub vs
--include-full-history.
All tests run offline. All openssl-dependent tests skipUnless _openssl_available().
All file writes scoped to tempfile.TemporaryDirectory; no contamination of
the user home or working directory.
Acceptance:
- [x] walnut_builder.build_walnut() generates v2 + v3 fixtures
- [x] FakeRelay implements upload/download/list_pending/delete in memory
- [x] All 12+ round-trip tests pass
- [x] Wrong-passphrase, corrupted tar, format-3.x, wrong RSA key, sha
mismatch all produce actionable errors
- [x] walnut_equal handles default ignore patterns
- [x] All tests run offline (no network)
- [x] RSA hybrid encryption/decryption functional, replaces deferrals
- [x] python3 -m unittest discover plugins/alive/tests passes (257)
Task: fn-7-7cw.11
Adds the v2→v3 migration golden-fixture test matrix and the LD22 tar
safety acceptance suite. Test count delta +37 (257→294, all under 2s).
Test files:
- test_p2p_v2_migration.py (17 tests) — golden-fixture migration matrix
built via walnut_builder, packaged through generate_manifest +
safe_tar_create, extracted via safe_tar_extract, then run through
migrate_v2_layout. Asserts post-shape + result counts.
- test_tar_safety.py (20 tests) — LD22 acceptance contract: hostile
tars built programmatically via tarfile.TarInfo + BytesIO, each
rejection case raises ValueError with empty dest post-exception.
Migration matrix coverage (all 11 cases from .12 spec table):
- simple-single-bundle, multi-bundle
- bundle-with-raw, bundle-with-observations
- bundle-with-sub-walnut (nested walnut preserved, NOT flattened)
- collision (alpha + live alpha/ → alpha-imported)
- tasks-md-with-assignments (@session)
- tasks-md-with-status-markers ([ ] [~] [x] → active/active+high/done)
- empty-bundles-dir (treated as v3)
- generated-dir (_kernel/_generated/ dropped)
- kernel-history-preserved (_kernel/history/chapter-01.md survives)
Plus: idempotency (byte-equal snapshot after second migrate), partial-
failure rollback (target untouched, staging preserved as
.alive-receive-incomplete-{ts}/), format-version gates with and without
manifest source_layout hint, v3→v2 downgrade documented behaviour, and
walnut_compare structural equality against a builder-emitted v3 tree.
Tar safety acceptance — 10 LD22 rejection cases:
1. path traversal (../etc/passwd)
2. absolute POSIX (/etc/passwd)
3. Windows drive letter (C:foo)
4. symlink member (any target, rejected outright)
5. hardlink member (any target, rejected outright)
6. device/fifo/block members
7. size bomb (cumulative > cap, patched cap for speed)
8. backslash in member name (foo\bar.md)
9. duplicate effective path (foo + ./foo)
10. unsupported member type + intermediate dot segment
Plus PAX header pass-through, regular file/dir baselines, and member-
count cap edge case.
LD22 hardening to alive-p2p.py:
The existing safe_tar_extract was significantly weaker than the LD22
spec — accepted symlinks pointing inside dest, accepted backslashes,
drive letters, duplicate effective paths, char devices (with crash),
and had no size or member-count caps. Replaced its second-pass
validation with the full LD22 pre-validation contract from the epic
spec: zero filesystem writes on any rejection, regular-file-and-dir
allowlist, PAX/GNU long-name metadata tolerated, all 10 rejection
cases handled with clean ValueError. safe_extractall is exposed as
an alias matching the LD22 spec name.
All 257 baseline tests still pass. New total: 294.
Self-review acceptance:
- [x] All 11 matrix cases in the documented table covered
- [x] Each case builds v2 walnut, packages, extracts, migrates, asserts
- [x] test_migration_idempotent passes (second migration is no-op,
byte-equal tree snapshot)
- [x] test_partial_failure_preserves_target passes (target never created,
.alive-receive-incomplete-{ts}/ preserved)
- [x] test_v2_package_with_source_layout_hint_accepts passes
- [x] test_v2_package_without_hint_structural_detection passes
- [x] v3→v2 downgrade handled gracefully (test pins documented behaviour:
manifest hint flows through, migrate is no-op, target ends up v3)
- [x] tasks_converted counts match expected values per case
- [x] All tests run offline, use tmp_path, complete in under 2 seconds
- [x] python3 -m unittest discover plugins/alive/tests passes (294 tests)
- [x] Tar safety suite all 10 LD22 rejection cases pass + PAX pass-through
Bugs discovered + fixed during testing:
- safe_tar_extract was missing 7 of 10 LD22 rejection paths. Hardened
in this commit to match the LD22 spec exactly. The receive pipeline
uses safe_tar_extract directly so this closes the gap end-to-end.
Task: fn-7-7cw.12
…plate (fn-7-7cw.13)
Ship-prep polish for the consolidation PR. No new functionality — version
bumps, v3 path staleness fixes in docs, and LD17 preferences template.
**LD14: plugin.json version bump**
- plugins/alive/.claude-plugin/plugin.json: 3.0.0 → 3.1.0 (minor, additive
P2P feature). Other manifest fields (description, author, homepage,
repository, license) verified current. plugin.json has NO hooks/commands/
agents/skills path fields — path-must-be-relative acceptance is
vacuously satisfied (convention-based discovery).
**LD17: templates/world/preferences.yaml — p2p block + discovery_hints**
- Added top-level commented `discovery_hints:` block per LD17 (the main
template had no such key previously).
- Added commented `p2p:` block with share_presets (internal/external),
relay config (url + token_env), auto_receive, signing_key_path,
require_signature — all opt-in, all default off.
**CLAUDE.md v3 path staleness (minimal surgical fixes, not a rewrite)**
- Frontmatter version 3.0.0 → 3.1.0 (consistency with plugin.json).
- "Read Before Speaking" sequence:
- item 2: `_kernel/now.json` annotated as computed projection via scripts/project.py
- dropped `bundles/*/tasks.md` entry (v3 uses tasks.json)
- new item 5: `_kernel/tasks.json` (v3 JSON task queue)
- item 7: `bundles/` → `{walnut}/{bundle}/context.manifest.yaml` with
note that v3 bundles live flat at walnut root, not under `bundles/`
- Skills section: "Fifteen Skills" → "Eighteen Skills", added /alive:share,
/alive:receive, /alive:relay entries. All other skill lines unchanged.
**Full-tree grep sweep (findings + fixes)**
grep -rn "stackwalnuts" plugins/alive/ → ZERO matches. Clean.
grep -rn "03_Inputs" plugins/alive/ → 5 matches, all intentional:
- skills/system-upgrade/SKILL.md:33,336,337,378 — v2→v3 migration docs (OK)
- skills/receive/SKILL.md:89 — documents old-path rejection (OK)
- hooks/scripts/alive-session-new.sh:346 — v2-world detector (OK)
- hooks/scripts/alive-session-new.sh:357 — "what's new" upgrade message (OK)
grep -rn "tasks\.md" plugins/alive/ → classified:
- ACTIVE V3 CODE PATHS (FIXED):
- skills/save/SKILL.md:25 — "Do NOT read bundles/*/tasks.md" reworded
to point at tasks.json + tasks.py
- templates/world/agents.md:26 — bundle state description reworded
to v3 (flat layout + tasks.json)
- skills/world/setup.md:485-518 — walnut scaffolder was creating
_kernel/_generated/ + bundles/ + tasks.md template. Now creates
flat _kernel/ with only key.md/log.md/insights.md; tasks.json,
completed.json and now.json created lazily by tasks.py/project.py.
- skills/world/setup.md:600-604 — reference table updated: now.json
path, tasks.json row added, bundles path shown as flat.
- skills/load-context/SKILL.md:144 — deep load path updated to
{walnut}/{name}/context.manifest.yaml (v3 flat) with v2 fallback note
- skills/search-world/SKILL.md:72 — example output "bundles/research/"
→ "research/" (v3 flat)
- MIGRATION DOCS / V2 FALLBACK (OK, kept):
- skills/system-upgrade/SKILL.md — documents v2→v3 conversion
- skills/receive/migration.md — documents receive-time v2 migration
- skills/create-walnut/migrate.md — documents legacy import
- scripts/alive-p2p.py — migrate_v2_layout() helper
- scripts/tasks.py:82-84 — warns when v2 tasks.md detected
- hooks/scripts/alive-context-watch.sh:186 — v2 file-change detector
- hooks/scripts/alive-session-new.sh:342 — v2-world detector
- tests/walnut_builder.py, tests/test_migrate.py,
tests/test_receive_migration.py, tests/test_p2p_v2_migration.py,
tests/test_walnut_builder.py — v2 fixture builders + migration tests
- skills/bundle/SKILL.md:93 — "Do NOT create tasks.md" negative guidance
- skills/system-cleanup/SKILL.md — v2-remnant detection
- rules/bundles.md:403 — legacy comparison table
- rules/squirrels.md:347 — v3 contract note
- templates/subagent-brief.md:43 — backward-compat note
grep -rn "bundles/" plugins/alive/ — remaining matches are in rules,
tests, migration code, legacy fallback scanners, or create-walnut/migrate.md
(all correctly contextualised). Active v3 code paths cleaned.
grep -rn "_generated" plugins/alive/ — remaining matches are in migration/
upgrade/backward-compat contexts only. rules/world.md:88 explicitly
documents fallback chain. Clean.
**Test suite**: 294 tests passing (stdlib unittest, unchanged count from .12).
**test_hooks_json.py**: 10 tests passing — LD15 description regex, LD16
relay-check registration on startup + resume, structure integrity all green.
**Self-review against acceptance criteria**:
- [x] plugin.json version = 3.1.0
- [x] CLAUDE.md "Read Before Speaking" no longer references bundles/*/tasks.md
- [x] CLAUDE.md bundle references are flat-layout aware
- [x] Zero `stackwalnuts` references in active plugins/alive/** files
- [x] `bundles/` references only in migration/backward-compat paths
- [x] `03_Inputs` references only in migration/detector code
- [x] `_generated` references only in migration/fallback chains
- [x] `tasks.md` references only in migration code + v2 detectors
- [x] plugin.json has no path fields (vacuously relative)
- [x] test suite passes (294/294)
- [x] test_hooks_json.py passes (10/10)
Task: fn-7-7cw.13
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Apr 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fresh-branch rewrite of the ALIVE P2P sharing layer targeting v3 architecture. Supersedes PR #27 (
fn-5-dof, v2-era) which became CONFLICTING after v3.0.0 landed on 2026-04-03.Preserves all high-level decisions from the fn-5-dof/fn-1-34r/fn-2-oiq/fn-6-7kn design iterations (GitHub relay transport, openssl-CLI dual encryption, .walnut tar.gz package, share presets, per-peer exclusions, discovery hints). Reimagines execution for v3 primitives (flat _kernel/, flat bundles, tasks.py, project.py, 03_Inbox).
Architectural decisions (LD1 to LD28)
The full design lives in
.flow/specs/fn-7-7cw.md(~1800 lines, 28 locked decisions) on Patrick's machine. Key decisions:source_layout: v2|v3hint (Docker schema2 precedent — receiver-side forward compat). NOT a 3.0 bump.bundles/container) regardless of sender's on-disk walnut layout. v2 legacy container format still supported on receive._kernel/imports.json. Partial-bundle receives trackapplied_bundlesper import_id; subset-of-union check enables "A applied then B applied" → future{A,B}request = no-op.tasks.py/project.py.full/snapshotrefuse existing targets;bundlescope preserves target_kernel/source files and validates walnut identity via byte-compare ofkey.md.import_id+ signature uses stdlibjson.dumpswith sort_keys + explicit separators + pre-sorted lists. NOT PyYAML (emitter fragility across versions).Salted__, RSA hybrid). RSA hybrid outer tar contains EXACTLYrsa-envelope-v1.json+payload.enc, nothing else.~/.alive/relay/keys/peers/<peer>.pem+index.jsonfor pubkey_id lookup. pubkey_id is 16 hex chars (NEVER base64).inbox/<sender>/subdirs, sparse clone for ops. FakeRelay abstraction for tests (zero network).fcntl.flockon POSIX, atomicmkdirfallback. Single exclusive lock model.A gap report seeded this rewrite: it lives in the stackwalnuts walnut at
bundles/p2p-v3-refactor/raw/2026-04-07-v3-gap-report.md(not in the claude-code repo — the walnut layer holds the design context, the repo holds the code).What ships
scripts/alive-p2p.py— ~6500 LOC. Lifted foundations (crypto, tar, sig), rewritten staging/manifest/migration, full share CLI + receive pipeline + auxiliary subcommands (info, log-import, unlock, verify, migrate)scripts/walnut_paths.py— NEW vendored helpers (resolve_bundle_path, find_bundles, scan_bundles)scripts/gh_client.py— NEW stdlib subprocess wrapper for gh CLI (mockable for tests)scripts/relay-probe.py— NEW canonical probe subcommandskills/share/,skills/receive/,skills/relay/— router + reference files (all routers under 500 lines, progressive disclosure preserved from fn-2-oiq)hooks/scripts/alive-relay-check.sh— NEW SessionStart hook, 10-min rate-limited, exits 0 in all expected pathshooks/hooks.json— description cleanup (removed hand-maintained hook count), alive-relay-check registered on startup + resume matcherstemplates/world/preferences.yaml— commented p2p: block + discovery_hints: top-level key.claude-plugin/plugin.json— version 3.0.0 → 3.1.0CLAUDE.md— surgical v3 path staleness fixes (read sequence, skill count, flat layout references)tests/— 14 test modules, 294 tests total, stdlib unittest only (no pytest dep)Testing
Coverage includes:
**, basename vs anchored, character classes)History
Supersedes #27 (fn-5-dof, v2-targeted). PR #27 had zero reviewer engagement and the branch was missing 1420 lines of v3 infrastructure (
tasks.py,project.py). Rebase was non-viable.Design history lives across the stackwalnuts walnut on Patrick's machine:
Reviewer notes
@benslockedin @willsupernormal — this is the successor to #27. Specific places I'd like your eyes:
encrypt_rsa_hybrid/decrypt_rsa_hybridinalive-p2p.py. Outer tar has EXACTLYrsa-envelope-v1.json+payload.enc(no marker file, no inner manifest at outer level). Multi-recipient viarecipients[]array. Does the envelope format match what you'd want long-term, or is there a cleaner way?scope: bundlerequires target walnut's_kernel/key.mdto byte-match the package's_kernel/key.md. Escape hatch viaALIVE_P2P_ALLOW_CROSS_WALNUT=1env var. Too strict?import_id = sha256_hex(canonical_manifest_bytes). Matches signed web token conventions. OK?inbox/<sender>/<package>.walnutper peer. Full spec inplugins/alive/skills/relay/reference.md. FakeRelay test harness intests/fake_relay.py.X | None, nolist[X]). Usestyping.Optional/List/Dict. Matches current tasks.py/project.py style.--strictescapes to exit 1. Swap failures exit 1 regardless. Does this match the hook-chain integration story you want?Deferrals:
os.kill(pid, 0)works; Windows falls back to mtime-based staleness). Easy follow-up PR.Generated with Claude Code via flow-next orchestration