Skip to content

feat(s15): stage adoption without erasing state or widening policy - #364

Merged
chayapats merged 11 commits into
mainfrom
jit/336-s15-adoption
Nov 3, 2026
Merged

chayapats merged 11 commits into
mainfrom
jit/336-s15-adoption

Conversation

@chayapats

@chayapats chayapats commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Stage explicit S14 adoption into a separate S15 store without rewriting original state, publishing private Bot data, or importing execution authority. S14 remains the default and omitted-runtime service configs remain legacy.

Closes #336

Implemented

  • Preserve and checksum the exact selected source, lifecycle ledger, configuration, native custody, Bot volumes/profiles and additional protected roots. Large originals use bounded streaming reads; credential-free history is generated only from in-memory copies.
  • Retain withdrawals, effect claims/outcomes, pending request identities and orphan usage. Componentwise reservation maxima become a nonrefundable POLICY floor. Historical requests cannot replay through adoption or POLICY ingress.
  • Keep imported history restricted and non-executable. POLICY starts empty; activation/release binds an explicit owner-installed proposal, exact source/target artifacts and current protected host evidence.
  • Fence old ordinary and maintenance writers, including existing handles. Persist intent/receipts and a separate worker-release barrier; deterministic crash/restart/SIGKILL fixtures recover the exact request. Rollback before activity reselects unchanged originals; newer unprojectable S15 facts refuse rollback under fences.
  • Add the explicit adoption --s15 --report --config <plan.json> and installer report paths plus owner JSON/i18n status data. Existing adoption/runtime-selection APIs are retained for [Frontier] [EVID15-01] Prove the protected boundaries and the GitHub scenario on the tested build #340 composition. The read-only s15AdoptionPort connects authenticated real status to the merged UX recovery page without adding control authority or recording activity.
  • Account for merged EFFECTS/GIT/MODEL/WORK/TEAM/MEM/CHAT/VM/DATA formats: existing journals are preserved and explicitly refused without invoking their recovery constructors. Unknown protected SQLite/browser continuity also refuses. Unexpected target journals cannot pass staging/activation under stale validation.

Supported refusals and pending work

Review fixes

  • Resume only the exact recorded discard from rolling_back, including interrupted discard before a policy store exists.
  • Apply unknown-store and sidecar checks to explicit protected roots while retaining named opaque S14 custody exceptions.
  • Reserve consumed takeover.input IDs from input/audit continuity with null payload/outcome digests; never import private input, process metadata or its HMAC.
  • Verify that the bound policy is usable before releasing workers. The authenticated read checks revocation/generation/expiry without recording S15 activity.
  • Refuse existing DATA disclosure stores without invoking recovery or changing lineage restrictions.

Validation

Tested commit: fb2afd15c5e20a9e91b85583fabb5bc37b14e6c6, based on merged main 8244179e69dd2ba41ff60237a51448d10c9dd778.

  • bun install --frozen-lockfile — passed.
  • bun run typecheck — passed.
  • Focused adoption, related-journal, review-regression and POLICY suites — 160 passed, 0 failed, 2,417 assertions across 8 files, including real recovery-page status/restart integration.
  • Review regressions cover recorded discard recovery (including SIGKILL and pre-candidate discard), external browser continuity, opaque native/profile exceptions, consumed takeover-input IDs without private input/HMAC, revoked/expired policy release, and DATA disclosure lineage preservation/refusal.
  • bun run test — 4,054 passed, 9 skipped, 0 failed; 1 snapshot and 257,074 assertions across 211 files (748.79s).
  • git diff --check — passed.

Contract: s15-state-transition/v1 executable revision 2, with unchanged FR/NFR requirements and S15-AC16. No live or paid tests were run.

@greptile-apps

greptile-apps Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 2/5

[High impact] The PR reaches the state and worker-release path for an explicitly selected S14 installation.

Fix discard recovery, external continuity checks, and historical request reservations before merging.

Findings

  1. P1 Interrupted discard cannot resume ▶
  2. P1 External browser continuity is overlooked ▶
  3. P1 Consumed request IDs become reusable ▶
  4. P2 Revoked policy still permits release ▶
Fix with agent prompt
### Issue 1
src/s15/adoption/service.ts:290
If the process crashes after `discard` saves `rolling_back`, retrying the exact request fails because this guard accepts only `preparing` or `validated`. The saved pending request also blocks replacement commands, leaving both runtimes fenced. Allow the recorded discard to resume from `rolling_back`, and add a crash test for this path.

### Issue 2
src/s15/adoption/source.ts:94-96
If browser continuity lives outside `stateDirectory`, even in an explicit `preservedPaths` root, `protectedState` is false. Its unversioned SQLite database is then merely checksummed, so adoption can proceed despite an unresolved browser action and no supported mapper. Apply the unknown-store check to explicit protected roots too, keeping the named native/profile exceptions separate.

### Issue 3
src/s15/adoption/source.ts:192-195
This query reserves only IDs from `command_receipts`, but S14 consumes `takeover.input` IDs separately in `computer_inputs`. Settled `succeeded` or `unknown` inputs pass adoption checks. After release, POLICY can accept those old IDs as fresh work requests because its guard checks only `source_requests`. Reserve the consumed input IDs too, without copying private input.

### Issue 4
src/s15/adoption/service.ts:284-287
If the owner revokes the validated policy before `release`, `currentPolicy()` still returns the same stored policy. Revocation changes the separate `policyRevoked` flag, so the hash check passes and adoption reports workers released. New work then fails with `authority_revoked`. Check that the bound policy is still usable before reporting release complete.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds explicit staged S14-to-S15 adoption while retaining original files and keeping POLICY empty until owner installation.

  • S14 history enters a restricted S15 store while the original files stay in place.
  • Owners stage S15 separately and release workers only under an installed policy.
  • Operators can request a read-only report for one named adoption plan.
  • S14 maintenance writes stop when a foreign adoption fence is present.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S14[S14 selected] --> Prepare[Prepare and fence source]
  Prepare --> Validate[Validate source and policy]
  Validate --> Activate[S15 selected, workers fenced]
  Activate --> Release[Release workers]
  Release --> Activity[Record activity before I/O]
  Validate --> Discard[Discard stage]
  Activate --> Rollback[Rollback before activity]
  Discard --> S14
  Rollback --> S14
  Activity --> Block[Refuse unsupported rollback]
Loading

Reviews (1) · Last reviewed commit: "fix(s15): refuse unprojected existing ru..." · Reviewed by Greptile

Comment thread src/s15/adoption/service.ts Outdated
stage.state = 'complete'; stage.revision++; finish(stage, actor); checkpoint('workers_released');
} else if (command.kind === 'rollback' || command.kind === 'discard') {
const discard = command.kind === 'discard';
if (discard ? !['preparing', 'validated'].includes(stage.state) : !['activating', 'active_fenced', 'complete', 'rolling_back', 'rolled_back'].includes(stage.state)) adoptionFail('transition_invalid_state');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Interrupted discard cannot resume

If the process crashes after discard saves rolling_back, retrying the exact request fails because this guard accepts only preparing or validated. The saved pending request also blocks replacement commands, leaving both runtimes fenced. Allow the recorded discard to resume from rolling_back, and add a crash test for this path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/s15/adoption/service.ts
Line: 290

Comment:
**Interrupted discard cannot resume**

If the process crashes after `discard` saves `rolling_back`, retrying the exact request fails because this guard accepts only `preparing` or `validated`. The saved pending request also blocks replacement commands, leaving both runtimes fenced. Allow the recorded discard to resume from `rolling_back`, and add a crash test for this path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/s15/adoption/source.ts Outdated
Comment on lines +94 to +96
const protectedState = containsPath(config.stateDirectory, path) && !containsPath(join(config.stateDirectory, 'native'), path) && !containsPath(`${o.source.database}.ops`, path) && path !== `${o.source.database}.lifecycle.sqlite`;
if (protectedState && /-(wal|shm|journal)$/.test(path)) adoptionFail('transition_sqlite_settlement_required');
const entry = fileEntry(path, protectedState); total += entry.bytes;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 External browser continuity is overlooked

If browser continuity lives outside stateDirectory, even in an explicit preservedPaths root, protectedState is false. Its unversioned SQLite database is then merely checksummed, so adoption can proceed despite an unresolved browser action and no supported mapper. Apply the unknown-store check to explicit protected roots too, keeping the named native/profile exceptions separate.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/s15/adoption/source.ts
Line: 94-96

Comment:
**External browser continuity is overlooked**

If browser continuity lives outside `stateDirectory`, even in an explicit `preservedPaths` root, `protectedState` is false. Its unversioned SQLite database is then merely checksummed, so adoption can proceed despite an unresolved browser action and no supported mapper. Apply the unknown-store check to explicit protected roots too, keeping the named native/profile exceptions separate.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/s15/adoption/source.ts Outdated
Comment on lines +192 to +195
const requests = (db.query('SELECT request_id,payload_hash,result_json FROM command_receipts ORDER BY request_id').all() as { request_id: string; payload_hash: string; result_json: string }[]).map(r => {
id(r.request_id); hash(r.payload_hash);
return { id: r.request_id, payloadHash: r.payload_hash, outcomeHash: hashBytes(r.result_json) };
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Consumed request IDs become reusable

This query reserves only IDs from command_receipts, but S14 consumes takeover.input IDs separately in computer_inputs. Settled succeeded or unknown inputs pass adoption checks. After release, POLICY can accept those old IDs as fresh work requests because its guard checks only source_requests. Reserve the consumed input IDs too, without copying private input.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/s15/adoption/source.ts
Line: 192-195

Comment:
**Consumed request IDs become reusable**

This query reserves only IDs from `command_receipts`, but S14 consumes `takeover.input` IDs separately in `computer_inputs`. Settled `succeeded` or `unknown` inputs pass adoption checks. After release, POLICY can accept those old IDs as fresh work requests because its guard checks only `source_requests`. Reserve the consumed input IDs too, without copying private input.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/s15/adoption/service.ts Outdated
Comment on lines +284 to +287
unchanged(stage); const p = currentPolicy(actor);
if (!p || !p.budget || p.expiresAt <= now() || p.budget.expiresAt <= now() || hashValue(p) !== stage.policyHash) adoptionFail('transition_policy_install_required');
begin();
stage.state = 'complete'; stage.revision++; finish(stage, actor); checkpoint('workers_released');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Revoked policy still permits release

If the owner revokes the validated policy before release, currentPolicy() still returns the same stored policy. Revocation changes the separate policyRevoked flag, so the hash check passes and adoption reports workers released. New work then fails with authority_revoked. Check that the bound policy is still usable before reporting release complete.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/s15/adoption/service.ts
Line: 284-287

Comment:
**Revoked policy still permits release**

If the owner revokes the validated policy before `release`, `currentPolicy()` still returns the same stored policy. Revocation changes the separate `policyRevoked` flag, so the hash check passes and adoption reports workers released. New work then fails with `authority_revoked`. Check that the bound policy is still usable before reporting release complete.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@chayapats
chayapats force-pushed the jit/336-s15-adoption branch from e95f4a1 to fb2afd1 Compare November 3, 2026 00:17
@chayapats
chayapats merged commit a9e25e3 into main Nov 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Frontier] [MIG15-01] Stage S15 adoption without erasing state or widening installed policy

1 participant