instar 0.28.102 → 0.28.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/instar/before-prompt-recall.js +90 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/jobMigrate.d.ts +46 -0
- package/dist/commands/jobMigrate.d.ts.map +1 -0
- package/dist/commands/jobMigrate.js +292 -0
- package/dist/commands/jobMigrate.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +88 -42
- package/dist/commands/server.js.map +1 -1
- package/dist/core/BackupManager.d.ts +27 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +56 -1
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/GitStateManager.d.ts +8 -0
- package/dist/core/GitStateManager.d.ts.map +1 -1
- package/dist/core/GitStateManager.js +31 -1
- package/dist/core/GitStateManager.js.map +1 -1
- package/dist/core/MigratorStepEngine.d.ts +170 -0
- package/dist/core/MigratorStepEngine.d.ts.map +1 -0
- package/dist/core/MigratorStepEngine.js +352 -0
- package/dist/core/MigratorStepEngine.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +76 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +252 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/PreCompactionFlush.d.ts +101 -0
- package/dist/core/PreCompactionFlush.d.ts.map +1 -0
- package/dist/core/PreCompactionFlush.js +282 -0
- package/dist/core/PreCompactionFlush.js.map +1 -0
- package/dist/core/PromptBuildRecall.d.ts +97 -0
- package/dist/core/PromptBuildRecall.d.ts.map +1 -0
- package/dist/core/PromptBuildRecall.js +152 -0
- package/dist/core/PromptBuildRecall.js.map +1 -0
- package/dist/core/selectIntelligenceProvider.d.ts +41 -0
- package/dist/core/selectIntelligenceProvider.d.ts.map +1 -0
- package/dist/core/selectIntelligenceProvider.js +89 -0
- package/dist/core/selectIntelligenceProvider.js.map +1 -0
- package/dist/lifeline/ServerSupervisor.d.ts +183 -0
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +443 -0
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/memory/NativeModuleHealer.d.ts +89 -0
- package/dist/memory/NativeModuleHealer.d.ts.map +1 -1
- package/dist/memory/NativeModuleHealer.js +371 -1
- package/dist/memory/NativeModuleHealer.js.map +1 -1
- package/dist/memory/SemanticMemory.d.ts +131 -0
- package/dist/memory/SemanticMemory.d.ts.map +1 -1
- package/dist/memory/SemanticMemory.js +315 -0
- package/dist/memory/SemanticMemory.js.map +1 -1
- package/dist/messaging/DeliveryRetryManager.d.ts +60 -0
- package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -1
- package/dist/messaging/DeliveryRetryManager.js +104 -0
- package/dist/messaging/DeliveryRetryManager.js.map +1 -1
- package/dist/monitoring/DegradationReporter.d.ts +164 -0
- package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
- package/dist/monitoring/DegradationReporter.js +309 -1
- package/dist/monitoring/DegradationReporter.js.map +1 -1
- package/dist/monitoring/ErrorCodeExtractor.d.ts +69 -0
- package/dist/monitoring/ErrorCodeExtractor.d.ts.map +1 -0
- package/dist/monitoring/ErrorCodeExtractor.js +131 -0
- package/dist/monitoring/ErrorCodeExtractor.js.map +1 -0
- package/dist/monitoring/PromptGate.d.ts +34 -0
- package/dist/monitoring/PromptGate.d.ts.map +1 -1
- package/dist/monitoring/PromptGate.js +81 -1
- package/dist/monitoring/PromptGate.js.map +1 -1
- package/dist/monitoring/Redactor.d.ts +58 -0
- package/dist/monitoring/Redactor.d.ts.map +1 -0
- package/dist/monitoring/Redactor.js +143 -0
- package/dist/monitoring/Redactor.js.map +1 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts +7 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts.map +1 -1
- package/dist/monitoring/probes/LifelineProbe.js +7 -0
- package/dist/monitoring/probes/LifelineProbe.js.map +1 -1
- package/dist/monitoring/probes/__shared.d.ts +50 -0
- package/dist/monitoring/probes/__shared.d.ts.map +1 -0
- package/dist/monitoring/probes/__shared.js +51 -0
- package/dist/monitoring/probes/__shared.js.map +1 -0
- package/dist/remediation/IntentJournal.d.ts +64 -0
- package/dist/remediation/IntentJournal.d.ts.map +1 -0
- package/dist/remediation/IntentJournal.js +118 -0
- package/dist/remediation/IntentJournal.js.map +1 -0
- package/dist/remediation/MachineLock.d.ts +137 -0
- package/dist/remediation/MachineLock.d.ts.map +1 -0
- package/dist/remediation/MachineLock.js +314 -0
- package/dist/remediation/MachineLock.js.map +1 -0
- package/dist/remediation/PrimaryAggregatorLease.d.ts +205 -0
- package/dist/remediation/PrimaryAggregatorLease.d.ts.map +1 -0
- package/dist/remediation/PrimaryAggregatorLease.js +450 -0
- package/dist/remediation/PrimaryAggregatorLease.js.map +1 -0
- package/dist/remediation/RemediationContext.d.ts +63 -0
- package/dist/remediation/RemediationContext.d.ts.map +1 -0
- package/dist/remediation/RemediationContext.js +109 -0
- package/dist/remediation/RemediationContext.js.map +1 -0
- package/dist/remediation/RemediationKeyVault.d.ts +123 -0
- package/dist/remediation/RemediationKeyVault.d.ts.map +1 -0
- package/dist/remediation/RemediationKeyVault.js +564 -0
- package/dist/remediation/RemediationKeyVault.js.map +1 -0
- package/dist/remediation/Remediator.d.ts +288 -0
- package/dist/remediation/Remediator.d.ts.map +1 -0
- package/dist/remediation/Remediator.js +520 -0
- package/dist/remediation/Remediator.js.map +1 -0
- package/dist/remediation/RemediatorBootstrap.d.ts +141 -0
- package/dist/remediation/RemediatorBootstrap.d.ts.map +1 -0
- package/dist/remediation/RemediatorBootstrap.js +255 -0
- package/dist/remediation/RemediatorBootstrap.js.map +1 -0
- package/dist/remediation/TrustElevationSource.d.ts +153 -0
- package/dist/remediation/TrustElevationSource.d.ts.map +1 -0
- package/dist/remediation/TrustElevationSource.js +208 -0
- package/dist/remediation/TrustElevationSource.js.map +1 -0
- package/dist/remediation/audit/AuditProjection.d.ts +37 -0
- package/dist/remediation/audit/AuditProjection.d.ts.map +1 -0
- package/dist/remediation/audit/AuditProjection.js +118 -0
- package/dist/remediation/audit/AuditProjection.js.map +1 -0
- package/dist/remediation/audit/AuditWriter.d.ts +79 -0
- package/dist/remediation/audit/AuditWriter.d.ts.map +1 -0
- package/dist/remediation/audit/AuditWriter.js +132 -0
- package/dist/remediation/audit/AuditWriter.js.map +1 -0
- package/dist/remediation/channels/CliApprovalChannel.d.ts +37 -0
- package/dist/remediation/channels/CliApprovalChannel.d.ts.map +1 -0
- package/dist/remediation/channels/CliApprovalChannel.js +45 -0
- package/dist/remediation/channels/CliApprovalChannel.js.map +1 -0
- package/dist/remediation/channels/TelegramApprovalChannel.d.ts +37 -0
- package/dist/remediation/channels/TelegramApprovalChannel.d.ts.map +1 -0
- package/dist/remediation/channels/TelegramApprovalChannel.js +45 -0
- package/dist/remediation/channels/TelegramApprovalChannel.js.map +1 -0
- package/dist/remediation/runbooks/db-corruption.d.ts +86 -0
- package/dist/remediation/runbooks/db-corruption.d.ts.map +1 -0
- package/dist/remediation/runbooks/db-corruption.js +190 -0
- package/dist/remediation/runbooks/db-corruption.js.map +1 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.d.ts +108 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.d.ts.map +1 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.js +172 -0
- package/dist/remediation/runbooks/messaging-delivery-failed.js.map +1 -0
- package/dist/remediation/runbooks/node-abi-mismatch.d.ts +70 -0
- package/dist/remediation/runbooks/node-abi-mismatch.d.ts.map +1 -0
- package/dist/remediation/runbooks/node-abi-mismatch.js +216 -0
- package/dist/remediation/runbooks/node-abi-mismatch.js.map +1 -0
- package/dist/remediation/runbooks/supervisor-preflight.d.ts +114 -0
- package/dist/remediation/runbooks/supervisor-preflight.d.ts.map +1 -0
- package/dist/remediation/runbooks/supervisor-preflight.js +299 -0
- package/dist/remediation/runbooks/supervisor-preflight.js.map +1 -0
- package/dist/scheduler/AgentMdAtomicSave.d.ts +85 -0
- package/dist/scheduler/AgentMdAtomicSave.d.ts.map +1 -0
- package/dist/scheduler/AgentMdAtomicSave.js +169 -0
- package/dist/scheduler/AgentMdAtomicSave.js.map +1 -0
- package/dist/scheduler/AgentMdReconcile.d.ts +62 -0
- package/dist/scheduler/AgentMdReconcile.d.ts.map +1 -0
- package/dist/scheduler/AgentMdReconcile.js +212 -0
- package/dist/scheduler/AgentMdReconcile.js.map +1 -0
- package/dist/scheduler/DisabledBodyDrift.d.ts +70 -0
- package/dist/scheduler/DisabledBodyDrift.d.ts.map +1 -0
- package/dist/scheduler/DisabledBodyDrift.js +149 -0
- package/dist/scheduler/DisabledBodyDrift.js.map +1 -0
- package/dist/scheduler/InstallBuiltinJobs.d.ts +53 -0
- package/dist/scheduler/InstallBuiltinJobs.d.ts.map +1 -0
- package/dist/scheduler/InstallBuiltinJobs.js +212 -0
- package/dist/scheduler/InstallBuiltinJobs.js.map +1 -0
- package/dist/scheduler/JobLoader.d.ts.map +1 -1
- package/dist/scheduler/JobLoader.js +48 -0
- package/dist/scheduler/JobLoader.js.map +1 -1
- package/dist/scheduler/MigrationInvariants.d.ts +62 -0
- package/dist/scheduler/MigrationInvariants.d.ts.map +1 -0
- package/dist/scheduler/MigrationInvariants.js +215 -0
- package/dist/scheduler/MigrationInvariants.js.map +1 -0
- package/dist/scheduler/MigrationLedger.d.ts +84 -0
- package/dist/scheduler/MigrationLedger.d.ts.map +1 -0
- package/dist/scheduler/MigrationLedger.js +114 -0
- package/dist/scheduler/MigrationLedger.js.map +1 -0
- package/dist/server/fileRoutes.d.ts.map +1 -1
- package/dist/server/fileRoutes.js +7 -0
- package/dist/server/fileRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +141 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +3 -2
- package/scripts/classify-default-drift.mjs +238 -0
- package/scripts/eli16-overview-check.mjs +85 -0
- package/scripts/instar-dev-precommit.js +60 -1
- package/scripts/lint-degradation-emit-sites.js +116 -0
- package/scripts/lint-no-direct-destructive.js +12 -0
- package/scripts/protect-migration-guarantee.js +61 -0
- package/scripts/regen-default-job-templates.mjs +111 -0
- package/scripts/verify-runbook-pr-signature.js +366 -0
- package/src/data/builtin-manifest.json +93 -93
- package/src/scaffold/bootstrap.ts +138 -0
- package/src/scaffold/keys/instar-release-pub.pem +3 -0
- package/src/scaffold/templates/jobs/instar/.gitkeep +0 -0
- package/src/scaffold/templates/jobs/instar/commitment-detection.md +28 -0
- package/src/scaffold/templates/jobs/instar/evolution-overdue-check.md +28 -0
- package/src/scaffold/templates/jobs/instar/evolution-proposal-evaluate.md +30 -0
- package/src/scaffold/templates/jobs/instar/evolution-proposal-implement.md +25 -0
- package/src/scaffold/templates/jobs/instar/health-check.md +14 -0
- package/src/scaffold/templates/jobs/instar/identity-review.md +42 -0
- package/src/scaffold/templates/jobs/instar/insight-harvest.md +31 -0
- package/src/scaffold/templates/jobs/instar/overseer-development.md +23 -0
- package/src/scaffold/templates/jobs/instar/overseer-guardian.md +28 -0
- package/src/scaffold/templates/jobs/instar/overseer-infrastructure.md +26 -0
- package/src/scaffold/templates/jobs/instar/overseer-learning.md +28 -0
- package/src/scaffold/templates/jobs/instar/overseer-maintenance.md +27 -0
- package/src/scaffold/templates/jobs/instar/reflection-trigger.md +40 -0
- package/src/scaffold/templates/jobs/instar/relationship-maintenance.md +16 -0
- package/src/scaffold/templates.ts +1616 -0
- package/upgrades/0.28.102.md +6 -7
- package/upgrades/0.28.103.md +471 -0
- package/upgrades/side-effects/a47-primary-aggregator-lease.md +82 -0
- package/upgrades/side-effects/agentmd-atomic-save.md +66 -0
- package/upgrades/side-effects/agentmd-loader-200jobs-perf-benchmark.md +50 -0
- package/upgrades/side-effects/agentmd-reconcile-boot-lifecycle.md +66 -0
- package/upgrades/side-effects/api-safety-guard.md +80 -0
- package/upgrades/side-effects/c1-runbook-pr-gate.md +83 -0
- package/upgrades/side-effects/disabled-body-drift-detection.md +59 -0
- package/upgrades/side-effects/drift-classifier-release-time.md +71 -0
- package/upgrades/side-effects/eli16-overview-required-gate.md +77 -0
- package/upgrades/side-effects/f1-remediation-key-vault.md +195 -0
- package/upgrades/side-effects/f2-redactor-errorcode-extractor.md +130 -0
- package/upgrades/side-effects/f3-degradation-reporter-shim.md +105 -0
- package/upgrades/side-effects/f4-machine-locks-intent-journal-audit.md +117 -0
- package/upgrades/side-effects/f5-trust-elevation-source.md +258 -0
- package/upgrades/side-effects/f6-supervisor-handshake.md +234 -0
- package/upgrades/side-effects/f7-post-update-migrator-atomic-step.md +129 -0
- package/upgrades/side-effects/f8-remediator-skeleton.md +153 -0
- package/upgrades/side-effects/f8-rest-enforcement.md +72 -0
- package/upgrades/side-effects/file-viewer-jobs-instar-never-editable.md +41 -0
- package/upgrades/side-effects/frontmatter-interpolation-breakout-test.md +48 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-2.md +78 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-3.md +76 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-4-endpoints.md +62 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-5.md +67 -0
- package/upgrades/side-effects/jobs-as-agentmd-phase-6.md +53 -0
- package/upgrades/side-effects/jobs-endpoints-auth-verification.md +52 -0
- package/upgrades/side-effects/migration-guarantee-suite.md +61 -0
- package/upgrades/side-effects/migration-runtime-invariant-gate.md +59 -0
- package/upgrades/side-effects/migration-telemetry-invariant-8.md +68 -0
- package/upgrades/side-effects/npm-pack-templates-smoke.md +51 -0
- package/upgrades/side-effects/openclaw-import-before-prompt-build.md +84 -0
- package/upgrades/side-effects/openclaw-import-pre-compaction-flush.md +85 -0
- package/upgrades/side-effects/prompt-gate-no-prompt-cache.md +258 -0
- package/upgrades/side-effects/tier2-degradation-reporter-live-wire.md +146 -0
- package/upgrades/side-effects/w1-node-abi-mismatch-runbook.md +154 -0
- package/upgrades/side-effects/w2-supervisor-preflight-runbook.md +176 -0
- package/upgrades/side-effects/w3-messaging-delivery-failed-runbook.md +185 -0
- package/upgrades/side-effects/w4-db-corruption-runbook.md +114 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Side-Effects Review — F-5: TrustElevationSource + AutonomyProfileLevel wiring
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `f5-trust-elevation-source`
|
|
4
|
+
**Date:** `2026-05-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Tier-2 foundation module from the Self-Healing Remediator v2 spec (per
|
|
11
|
+
`docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` "Trust elevation policy"
|
|
12
|
+
section + amendments A11, A22, A25, A41, A53, A57, A59). Three new modules
|
|
13
|
+
under `src/remediation/`:
|
|
14
|
+
|
|
15
|
+
- `src/remediation/TrustElevationSource.ts` — authoritative gate for
|
|
16
|
+
runbook lifecycle transitions. Implements the asymmetric trust-elevation
|
|
17
|
+
table from the spec: pessimistic-quarantine always-allowed, collaborative
|
|
18
|
+
trust minimum for upward transitions, 48h fresh-trace + 1-week history
|
|
19
|
+
for registered→live, two-distinct-kind-channel rule for essential
|
|
20
|
+
un-quarantine (A53), and source-only transitions for
|
|
21
|
+
proposal→registered, live→deprecated, deprecated→removed.
|
|
22
|
+
- `src/remediation/channels/TelegramApprovalChannel.ts` — F-5 stub
|
|
23
|
+
implementation of `TrustedApprovalChannel`. Exposes the shape the real
|
|
24
|
+
Telegram-countersignature verification (A41) will plug into, with a
|
|
25
|
+
deterministic seeded-map for test fixtures.
|
|
26
|
+
- `src/remediation/channels/CliApprovalChannel.ts` — F-5 stub
|
|
27
|
+
implementation of `TrustedApprovalChannel` for the
|
|
28
|
+
`instar doctor confirm-unquarantine` signed-CLI second-factor path
|
|
29
|
+
(A53 option 1). Same seeded-map shape.
|
|
30
|
+
|
|
31
|
+
This PR ships the policy module ONLY. No surface (dispatcher F-8,
|
|
32
|
+
un-quarantine endpoint A25, dashboard toggle) wires into the source in
|
|
33
|
+
this PR. F-5 is consumed by F-8's runbook-lifecycle wiring and by the
|
|
34
|
+
Tier-2 `POST /remediation/unquarantine/:runbookId` endpoint in
|
|
35
|
+
subsequent PRs.
|
|
36
|
+
|
|
37
|
+
The change is foundational infrastructure: it adds capability surface, it
|
|
38
|
+
does not remove or replace any existing decision point.
|
|
39
|
+
|
|
40
|
+
## Decision-point inventory
|
|
41
|
+
|
|
42
|
+
- `TrustElevationSource.canTransition()` — **add** — gates whether a
|
|
43
|
+
runbook lifecycle transition is permitted given (a) the current
|
|
44
|
+
autonomy profile, (b) supplied dry-run context, (c) approvals collected
|
|
45
|
+
via configured channels. Returns `{allowed, reason}`; never mutates
|
|
46
|
+
registry state.
|
|
47
|
+
- `TrustElevationSource.requireSecondChannel()` — **add** — gates whether
|
|
48
|
+
the configured channels are sufficient for an essential-runbook
|
|
49
|
+
un-quarantine (A53's "real second factor" rule, distinct-kind).
|
|
50
|
+
- `TelegramApprovalChannel.verifyApproval()` — **add** — stub: returns
|
|
51
|
+
approval for seeded entries only. Real Telegram countersignature
|
|
52
|
+
verification (A41) is a Tier-2 follow-up.
|
|
53
|
+
- `CliApprovalChannel.verifyApproval()` — **add** — stub: returns
|
|
54
|
+
confirmation for seeded entries only. Real `instar doctor
|
|
55
|
+
confirm-unquarantine` signed-CLI integration is a Tier-2 follow-up.
|
|
56
|
+
|
|
57
|
+
All decision points operate on caller-supplied policy inputs
|
|
58
|
+
(autonomy profile, dry-run trace age + history span, essential bit,
|
|
59
|
+
channel approvals). They do not contain content classification,
|
|
60
|
+
heuristics, or "guess intent from text" logic.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 1. Over-block
|
|
65
|
+
|
|
66
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
67
|
+
|
|
68
|
+
- `canTransition('proposal-to-registered', ...)` ALWAYS refuses. By
|
|
69
|
+
design — the spec deliberately offers no programmatic path. A human
|
|
70
|
+
must run a `/instar-dev` commit + spec-converge approval. The source
|
|
71
|
+
cannot become a backdoor.
|
|
72
|
+
- `canTransition('live-to-deprecated' | 'deprecated-to-removed', ...)`
|
|
73
|
+
ALWAYS refuses. Same reason: source change only.
|
|
74
|
+
- `registered-to-live` refuses without `dryRunTraceAge` /
|
|
75
|
+
`dryRunHistoryDays`. The dispatcher is expected to supply both from
|
|
76
|
+
the audit projection (F-4); a caller that omits them is a programmer
|
|
77
|
+
error. Refusing with an explicit `missing-...` reason is preferable to
|
|
78
|
+
silently approving.
|
|
79
|
+
- Below-`collaborative` profile refuses ALL upward transitions even
|
|
80
|
+
when channels approve. Spec-mandated. The profile is the user-set
|
|
81
|
+
policy on how much authority the agent gets; it sits ABOVE channel
|
|
82
|
+
approvals. A user explicitly setting `cautious` SHOULD see the
|
|
83
|
+
agent refuse to un-quarantine even if approval comes through.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. Under-block
|
|
88
|
+
|
|
89
|
+
**What failure modes does this still miss?**
|
|
90
|
+
|
|
91
|
+
- **Stub channels are not real.** F-5 ships the abstraction with
|
|
92
|
+
deterministic seeded-map verification. The real verification (A41
|
|
93
|
+
Telegram-countersignature with proposalId-binding + user_id principal +
|
|
94
|
+
message-id watermark; A53 CLI signing key) lives in the channel
|
|
95
|
+
implementations that ship later. A misconfigured agent that wires a
|
|
96
|
+
stub into production would silently approve nothing; the dispatcher
|
|
97
|
+
surfaces this as `no-matching-...-countersignature` audit-logged refusal.
|
|
98
|
+
- **Channel-kind collision.** The A53 "different-kind" rule keys on the
|
|
99
|
+
channel's `kind` string. Two implementations using the same `kind`
|
|
100
|
+
(e.g., two different Telegram bots) would falsely satisfy the
|
|
101
|
+
two-channel rule if the source counted instances. The implementation
|
|
102
|
+
counts distinct `kind` strings, so this is closed at the source. But
|
|
103
|
+
a channel-implementer that picks a clashing `kind` would create a
|
|
104
|
+
silent collapse — channel-implementer review is the mitigation.
|
|
105
|
+
- **Replay across transitions.** F-5 forwards the channel's `messageId`
|
|
106
|
+
unchanged. The channel implementation MUST enforce the A41 watermark
|
|
107
|
+
`(proposalId, messageId)` tuple; F-5 does not duplicate that check.
|
|
108
|
+
This is the right layer — the source decides "did enough channels
|
|
109
|
+
approve" given verified results; the channel decides "is this
|
|
110
|
+
approval real and non-replayed."
|
|
111
|
+
- **Race between concurrent un-quarantine attempts.** F-5 evaluates
|
|
112
|
+
channel approvals in `Promise.all`. Two concurrent dispatcher calls
|
|
113
|
+
for the same runbook would each issue a separate `verifyApproval`. The
|
|
114
|
+
channel implementation's watermark catches replay; the dispatcher's
|
|
115
|
+
per-runbook lock (F-4 `MachineLock`) prevents two un-quarantines from
|
|
116
|
+
taking effect. F-5 trusts those upstream gates.
|
|
117
|
+
- **Trust-profile race.** If `AutonomyProfileManager.setProfile()` flips
|
|
118
|
+
during a `canTransition` call, the source captured `profile` in the
|
|
119
|
+
constructor. The dispatcher is expected to construct a fresh
|
|
120
|
+
`TrustElevationSource` per dispatch (or re-read profile from the
|
|
121
|
+
manager). F-5 does not subscribe to profile changes.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 3. Level-of-abstraction fit
|
|
126
|
+
|
|
127
|
+
**Is this at the right layer?**
|
|
128
|
+
|
|
129
|
+
Yes. F-5 is a **policy** module:
|
|
130
|
+
|
|
131
|
+
- It decides "is this transition permitted right now given my
|
|
132
|
+
inputs"; it does not mutate state, dispatch runbooks, write
|
|
133
|
+
registry, or talk to the network.
|
|
134
|
+
- The dispatcher (F-8) and the un-quarantine endpoint (A25) are the
|
|
135
|
+
enforcement points: they call `canTransition`, get a verdict, and
|
|
136
|
+
either proceed or audit-log + refuse.
|
|
137
|
+
- The channels are **mechanism**: they verify cryptographic /
|
|
138
|
+
protocol-specific approval proofs and return a boolean.
|
|
139
|
+
|
|
140
|
+
Three-layer split (mechanism → policy → enforcement) is the spec-
|
|
141
|
+
mandated layering. F-5 sits in the middle layer alone — channels and
|
|
142
|
+
enforcement live elsewhere.
|
|
143
|
+
|
|
144
|
+
The `AutonomyProfileLevel` type is re-exported from
|
|
145
|
+
`src/core/types.ts` (canonical owner). F-5 does not duplicate or
|
|
146
|
+
redefine the type; it imports the same enum the rest of the codebase
|
|
147
|
+
uses via `AutonomyProfileManager`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. Signal vs authority compliance
|
|
152
|
+
|
|
153
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
154
|
+
|
|
155
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
156
|
+
|
|
157
|
+
- [x] Yes — but the logic is a smart gate operating on cryptographic /
|
|
158
|
+
policy primitives.
|
|
159
|
+
- [ ] ⚠️ Yes, with brittle logic — STOP.
|
|
160
|
+
|
|
161
|
+
The "blocks" in this change are:
|
|
162
|
+
|
|
163
|
+
1. Profile-level comparison against a numeric ordering (`cautious=0,
|
|
164
|
+
supervised=1, collaborative=2, autonomous=3`). Deterministic.
|
|
165
|
+
2. Trace-age numeric comparison against `FRESH_TRACE_MAX_AGE_MS`.
|
|
166
|
+
Deterministic.
|
|
167
|
+
3. History-span numeric comparison against `MIN_DRY_RUN_HISTORY_DAYS`.
|
|
168
|
+
Deterministic.
|
|
169
|
+
4. Channel-approval boolean count + distinct-kind set size.
|
|
170
|
+
Deterministic, sourced from channel verifications.
|
|
171
|
+
5. Transition enum exhaustive switch. Deterministic.
|
|
172
|
+
|
|
173
|
+
None of these are "string match on free-text"-class brittle filters.
|
|
174
|
+
The signal-vs-authority anti-pattern is heuristic detectors holding
|
|
175
|
+
blocking power; F-5's gates are policy primitives operating on
|
|
176
|
+
authoritative inputs. The smart gate (the dispatcher / un-quarantine
|
|
177
|
+
endpoint) is the consumer that decides *whether to attempt* a
|
|
178
|
+
transition; F-5 only encodes the trust-elevation table.
|
|
179
|
+
|
|
180
|
+
The CHANNEL implementations are where signal-vs-authority matters most.
|
|
181
|
+
F-5's stubs are deterministic seeded-map lookups; the real Telegram /
|
|
182
|
+
CLI channels MUST enforce cryptographic verification (HMAC, signature,
|
|
183
|
+
watermark) — not string match on user reply text. The
|
|
184
|
+
`TrustedApprovalChannel` interface returns a `principalUserId` so the
|
|
185
|
+
A41 user_id-principal binding is preserved end-to-end.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 5. Interactions
|
|
190
|
+
|
|
191
|
+
**Does this interact with existing checks, recovery paths, or
|
|
192
|
+
infrastructure?**
|
|
193
|
+
|
|
194
|
+
- **Shadowing:** `src/core/TrustElevationTracker.ts` exists already.
|
|
195
|
+
Despite the similar name, that module tracks dashboard-driven trust
|
|
196
|
+
elevation events at the autonomy-profile layer. F-5 is the
|
|
197
|
+
remediator-specific runbook-lifecycle policy module. They share the
|
|
198
|
+
`AutonomyProfileLevel` type but no decision-point overlap; the
|
|
199
|
+
tracker is the SOURCE of the profile that F-5 READS.
|
|
200
|
+
- **Race with adjacent cleanup:** None. F-5 has no on-disk state, no
|
|
201
|
+
background timers, no event listeners. Its only mutable state is the
|
|
202
|
+
channel list captured at construction.
|
|
203
|
+
- **Double-fire:** `canTransition` is pure with respect to F-5 state
|
|
204
|
+
(no caching, no memo). Channels are responsible for their own
|
|
205
|
+
idempotency / replay defence. The dispatcher's `MachineLock`
|
|
206
|
+
serialises actual state mutations.
|
|
207
|
+
- **Interaction with AutonomyProfileManager.** F-5 captures the
|
|
208
|
+
profile at construction. The dispatcher SHOULD construct a fresh
|
|
209
|
+
`TrustElevationSource` per dispatch (or re-derive). This is
|
|
210
|
+
documented in the module header.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 6. External surfaces
|
|
215
|
+
|
|
216
|
+
**Does this change anything visible to other agents, other users,
|
|
217
|
+
other systems?**
|
|
218
|
+
|
|
219
|
+
- No on-disk state. F-5 is in-memory only.
|
|
220
|
+
- No HTTP routes. The A25 `POST /remediation/unquarantine/:runbookId`
|
|
221
|
+
endpoint that consumes F-5 ships in a follow-up PR.
|
|
222
|
+
- No Telegram surfaces. The Telegram channel implementation is a stub;
|
|
223
|
+
the real Telegram-countersignature wiring ships in a follow-up.
|
|
224
|
+
- No dashboard tabs.
|
|
225
|
+
- New public types: `RunbookTransition`, `TrustedApprovalChannel`,
|
|
226
|
+
`TrustedApprovalVerifyInput`, `TrustedApprovalVerifyResult`,
|
|
227
|
+
`CanTransitionContext`, `CanTransitionResult`,
|
|
228
|
+
`TrustElevationSourceOpts`. All under `src/remediation/`.
|
|
229
|
+
- New configuration surface: `remediation.approvalChannel.primary`
|
|
230
|
+
(per A59) is NOT yet read by F-5; the channel array is supplied
|
|
231
|
+
explicitly by the constructing caller. Config wiring lives at the
|
|
232
|
+
point of dispatcher construction, in a follow-up PR.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 7. Rollback cost
|
|
237
|
+
|
|
238
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
239
|
+
|
|
240
|
+
Trivially low. No consumers of these modules ship in this PR — the
|
|
241
|
+
exported symbols are new and not yet imported anywhere in `src/`. A
|
|
242
|
+
`git revert` removes them with zero state-migration cost. No on-disk
|
|
243
|
+
files are created.
|
|
244
|
+
|
|
245
|
+
If a future consumer-PR exposes a bug in the policy logic, the back-out
|
|
246
|
+
path is "revert the consumer PR" — F-5 in isolation has no runtime
|
|
247
|
+
effect.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Reviewer concurrence (Phase 5)
|
|
252
|
+
|
|
253
|
+
Not required. This change touches no block/allow surface for messaging,
|
|
254
|
+
no session lifecycle, no coherence/sentinel/gate authority. It is
|
|
255
|
+
foundational policy infrastructure with no live consumers in this PR.
|
|
256
|
+
The dispatcher (F-8 Tier-2) and the un-quarantine endpoint (A25
|
|
257
|
+
follow-up) are the enforcement points; their PRs will require their own
|
|
258
|
+
side-effects reviews when wiring lands.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Side-Effects Review — F-6 ServerSupervisor handshake
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `f6-supervisor-handshake`
|
|
4
|
+
**Date:** 2026-05-13
|
|
5
|
+
**Author:** echo (instar-developing agent)
|
|
6
|
+
**Second-pass reviewer:** not required
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Extends `src/lifeline/ServerSupervisor.ts` with the Remediator ↔ Supervisor
|
|
11
|
+
handshake described in `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md`
|
|
12
|
+
(§A15 partial-upgrade rule, carried forward from v1 §Supervisor
|
|
13
|
+
coordination) and `docs/specs/SELF-HEALING-REMEDIATOR-V3-CONSOLIDATED-SPEC.md`
|
|
14
|
+
(§3 state-file taxonomy: `supervisor-handshake.json`, HMAC-extended
|
|
15
|
+
`restart-requested.json`; §9 Tier-2 sequencing). Adds:
|
|
16
|
+
|
|
17
|
+
- `ServerSupervisor.HANDSHAKE_PROTOCOL_VERSION = 1` (static).
|
|
18
|
+
- `RestartRequestedPayload`, `RestartRequestedReply`, `RegisteredRemediator`
|
|
19
|
+
public types.
|
|
20
|
+
- `ServerSupervisor.registerRemediator(remediator)` — writes
|
|
21
|
+
`<stateDir>/state/supervisor-handshake.json` (`{version,
|
|
22
|
+
supervisorBuildId, writtenAt}`).
|
|
23
|
+
- `ServerSupervisor.handleRestartRequested(payload)` — verifies HMAC,
|
|
24
|
+
handshake-version, staleness (5-minute window), and blastRadius
|
|
25
|
+
(`process | machine` only — `fleet` is refused for Tier-2). On accept,
|
|
26
|
+
tracks the request and initiates a `performGracefulRestart` cycle.
|
|
27
|
+
- `notifyPendingRemediatorRequestsOnHealthy()` — fires
|
|
28
|
+
`remediator.onRestartComplete({requestId})` once the next healthy tick
|
|
29
|
+
lands after a serverRestarting → healthy transition.
|
|
30
|
+
- `canonicalRestartRequestedBody(payload)` exported helper —
|
|
31
|
+
deterministic byte serialization used as HMAC input on both sides.
|
|
32
|
+
|
|
33
|
+
The existing `private preflightSelfHeal()` is unchanged; W-2 will wrap
|
|
34
|
+
it. The existing `restart-requested.json` flag path is unchanged (the
|
|
35
|
+
in-process handshake is the canonical F-6 surface; the file-based flag
|
|
36
|
+
remains for AutoUpdater and is co-owned per v3 §3).
|
|
37
|
+
|
|
38
|
+
Files touched: `src/lifeline/ServerSupervisor.ts`,
|
|
39
|
+
`tests/unit/ServerSupervisor-handshake.test.ts` (new, 9 tests),
|
|
40
|
+
`upgrades/NEXT.md`, this artifact.
|
|
41
|
+
|
|
42
|
+
## Decision-point inventory
|
|
43
|
+
|
|
44
|
+
- `handleRestartRequested` — **add** — verifies signed restart requests.
|
|
45
|
+
This holds blocking authority: a rejection refuses to restart the
|
|
46
|
+
server. Each rejection branch is a deterministic check on
|
|
47
|
+
cryptographic state (HMAC, monotonic clock, declared protocol
|
|
48
|
+
version, declared blast radius), NOT a heuristic message classifier.
|
|
49
|
+
See section 4 for signal-vs-authority analysis.
|
|
50
|
+
- `registerRemediator` — **add** — idempotent registration; no
|
|
51
|
+
authority. Side-effects writes `supervisor-handshake.json`.
|
|
52
|
+
- `notifyPendingRemediatorRequestsOnHealthy` — **add** — fires
|
|
53
|
+
callbacks. Idempotent (entries deleted after first call). No
|
|
54
|
+
authority.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 1. Over-block
|
|
59
|
+
|
|
60
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
61
|
+
|
|
62
|
+
- `blastRadius: 'fleet'` is rejected. Tier-2's ServerSupervisor only
|
|
63
|
+
handles `process` and `machine` restarts. A fleet-wide restart is the
|
|
64
|
+
coordination-protocol surface's responsibility, not the supervisor's.
|
|
65
|
+
Refusal is by design and matches v3 §9 Tier-3 deferral; Remediator
|
|
66
|
+
must not issue fleet-scope restart-requested in Tier-2 anyway.
|
|
67
|
+
- Clock skew > 5 minutes (in either direction) rejects valid requests.
|
|
68
|
+
This is the documented staleness window per the F-6 spec; widening it
|
|
69
|
+
would weaken replay defense. Operators with persistent clock skew
|
|
70
|
+
receive `stale: ageMs=…` in the reject reason; the Remediator
|
|
71
|
+
surfaces this in audit + alerts via the higher-level dispatch path.
|
|
72
|
+
- Handshake version mismatch is exact-equality only. A future v2
|
|
73
|
+
protocol must be negotiated by bumping `HANDSHAKE_PROTOCOL_VERSION`
|
|
74
|
+
on both sides; legacy Remediators speaking v1 will be rejected. This
|
|
75
|
+
is the A15 contract — partial-upgrade DoS is prevented by refusing,
|
|
76
|
+
not by best-effort interpolation.
|
|
77
|
+
|
|
78
|
+
## 2. Under-block
|
|
79
|
+
|
|
80
|
+
**What failure modes does this still miss?**
|
|
81
|
+
|
|
82
|
+
- The `monotonicTs` field is informational; the HMAC covers it but the
|
|
83
|
+
supervisor does NOT enforce monotonic ordering across requests. A
|
|
84
|
+
per-Remediator monotonic counter would close the residual replay
|
|
85
|
+
window inside the 5-minute staleness budget. F-8 (rest) owns the
|
|
86
|
+
per-surface monotonic counter (A42); F-6 deliberately punts.
|
|
87
|
+
- The supervisor accepts only the registered Remediator's leaf key. If
|
|
88
|
+
the Remediator's RemediationKeyVault is rotated mid-flight, an
|
|
89
|
+
in-flight request signed with the old leaf will fail HMAC
|
|
90
|
+
verification. This is expected — the overlap window is owned at the
|
|
91
|
+
vault rotation layer (per F-1's `rotateContext` contract).
|
|
92
|
+
- `RegisteredRemediator.getCapabilityLeafKey()` is called per
|
|
93
|
+
verification — the supervisor does not cache. This is intentional
|
|
94
|
+
(lets the Remediator rotate without re-registration) but pushes a
|
|
95
|
+
small constant cost onto each call. Negligible at expected throughput
|
|
96
|
+
(≤ 1 restart-requested per process per ~24h per A7 cross-process cap).
|
|
97
|
+
- File-based `restart-requested.json` path is unchanged in this PR.
|
|
98
|
+
Adding HMAC to the file shape is described in v3 §9 ("HMAC required on
|
|
99
|
+
ANY plannedRestart:true post-F-6"); the in-process handshake is the
|
|
100
|
+
Tier-2 canonical surface and file-shape HMAC migration tracks under
|
|
101
|
+
the AutoUpdater path, not F-6 proper. NEXT.md documents the gap.
|
|
102
|
+
|
|
103
|
+
## 3. Level-of-abstraction fit
|
|
104
|
+
|
|
105
|
+
**Is this at the right layer?**
|
|
106
|
+
|
|
107
|
+
Yes. F-6 lives in `src/lifeline/ServerSupervisor.ts` because the
|
|
108
|
+
supervisor is the only authority that can request a tmux-session
|
|
109
|
+
restart safely. The handshake protocol surface is a thin layer on top
|
|
110
|
+
of the supervisor's existing `performGracefulRestart()`; it does NOT
|
|
111
|
+
re-implement restart logic, doesn't reach into runbook internals, and
|
|
112
|
+
doesn't speak any domain vocabulary above
|
|
113
|
+
`{requestId, runbookId, attemptId, blastRadius}`.
|
|
114
|
+
|
|
115
|
+
The `canonicalRestartRequestedBody()` exported helper deliberately
|
|
116
|
+
lives at the bottom of the same file so the wire format has one
|
|
117
|
+
canonical owner — both the supervisor's verify path and the
|
|
118
|
+
Remediator's sign path (in F-8 rest) call the same function. Cross-file
|
|
119
|
+
divergence is the most common HMAC-protocol bug; co-locating prevents
|
|
120
|
+
it.
|
|
121
|
+
|
|
122
|
+
## 4. Signal vs authority compliance
|
|
123
|
+
|
|
124
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
125
|
+
|
|
126
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
127
|
+
|
|
128
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
129
|
+
- [ ] No — this change has no block/allow surface.
|
|
130
|
+
- [x] Yes — but the logic is a smart gate with full conversational context.
|
|
131
|
+
- [ ] Yes, with brittle logic — STOP.
|
|
132
|
+
|
|
133
|
+
`handleRestartRequested` is a blocking gate. Each rejection condition
|
|
134
|
+
is a deterministic check on cryptographic + structural state, not a
|
|
135
|
+
heuristic message classifier:
|
|
136
|
+
|
|
137
|
+
1. HMAC verification via `crypto.timingSafeEqual` — pure crypto.
|
|
138
|
+
2. Handshake-version equality — integer compare.
|
|
139
|
+
3. `requestedAt` staleness — wall-clock arithmetic on a signed field.
|
|
140
|
+
4. `blastRadius` allowlist — string-set membership on a signed field.
|
|
141
|
+
5. Required-field shape check — typeof guards.
|
|
142
|
+
|
|
143
|
+
Every gated field is HMAC-covered by `canonicalRestartRequestedBody()`,
|
|
144
|
+
so an attacker who shifts any field invalidates the signature. The
|
|
145
|
+
gate has the FULL context needed to decide — the canonical body IS the
|
|
146
|
+
context — and the decision is reproducible. This matches the
|
|
147
|
+
"signal-vs-authority" contract: the gate that holds blocking authority
|
|
148
|
+
has the full structured payload, not a free-text excerpt.
|
|
149
|
+
|
|
150
|
+
## 5. Interactions
|
|
151
|
+
|
|
152
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
153
|
+
|
|
154
|
+
- **`performGracefulRestart`:** `handleRestartRequested` calls the
|
|
155
|
+
existing method on accept. Maintenance wait is pre-set so existing
|
|
156
|
+
`handleUnhealthy`/serverDown suppression behaves as it does for
|
|
157
|
+
AutoUpdater-driven restarts.
|
|
158
|
+
- **`restart-requested.json` file path:** Untouched. Both the file path
|
|
159
|
+
(AutoUpdater) and the in-process path (Remediator) trigger the same
|
|
160
|
+
`performGracefulRestart`. The two paths are independent and cannot
|
|
161
|
+
race destructively — `performGracefulRestart` is idempotent at the
|
|
162
|
+
tmux-session level.
|
|
163
|
+
- **Pending-request notification:** Wired into the existing healthy-
|
|
164
|
+
tick branch of the health check loop. Idempotent (clears the map
|
|
165
|
+
after firing). If `onRestartComplete` throws, the supervisor logs and
|
|
166
|
+
continues — a faulty Remediator callback cannot wedge the supervisor.
|
|
167
|
+
- **`debug-restart-request.json`:** Unrelated path; doctor-session
|
|
168
|
+
HMAC uses a different secret (`setDoctorSessionSecret`) and a
|
|
169
|
+
different file. No interaction.
|
|
170
|
+
- **State-file: `supervisor-handshake.json`:** Newly written by
|
|
171
|
+
`registerRemediator` + `setSupervisorBuildId`. Per v3 §3 it is NOT
|
|
172
|
+
backed up and NOT git-synced. The path matches v3 §3 verbatim.
|
|
173
|
+
- **`RemediationKeyVault` (F-1):** F-6 takes the leaf key from the
|
|
174
|
+
registered Remediator via callback, NOT directly from the vault.
|
|
175
|
+
This keeps the supervisor decoupled from F-1's API surface; the
|
|
176
|
+
Remediator (which owns the vault lifecycle) injects the key. If F-1
|
|
177
|
+
rotates the context, the next registration / next call sees the new
|
|
178
|
+
key.
|
|
179
|
+
|
|
180
|
+
## 6. External surfaces
|
|
181
|
+
|
|
182
|
+
**Does this change anything visible to other agents, other users, other systems?**
|
|
183
|
+
|
|
184
|
+
- **State file:** `<stateDir>/state/supervisor-handshake.json` (new).
|
|
185
|
+
JSON `{version, supervisorBuildId, writtenAt}`. World-readable by
|
|
186
|
+
default (same as sibling files in the same dir). No secrets in the
|
|
187
|
+
body.
|
|
188
|
+
- **Public types:** `RestartRequestedPayload`,
|
|
189
|
+
`RestartRequestedReply`, `RegisteredRemediator`,
|
|
190
|
+
`canonicalRestartRequestedBody()` exported from
|
|
191
|
+
`src/lifeline/ServerSupervisor.ts`. These are stable API surfaces
|
|
192
|
+
that F-8-rest will consume; bump `HANDSHAKE_PROTOCOL_VERSION` on any
|
|
193
|
+
wire-format change.
|
|
194
|
+
- **Events:** No new EventEmitter events. The existing
|
|
195
|
+
`serverRestarting` event still fires from `performGracefulRestart`.
|
|
196
|
+
`onRestartComplete` is delivered via callback, not event, because it
|
|
197
|
+
is a per-request notification with a `requestId` correlator (events
|
|
198
|
+
fan out; callbacks correlate).
|
|
199
|
+
- **No telegram, slack, or external API calls** added.
|
|
200
|
+
|
|
201
|
+
## 7. Rollback cost
|
|
202
|
+
|
|
203
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
204
|
+
|
|
205
|
+
`git revert` on the F-6 PR removes the surface. No data migration:
|
|
206
|
+
|
|
207
|
+
- `supervisor-handshake.json` is regenerated on next registration; if
|
|
208
|
+
no consumer registers (post-revert state), the file is stale but inert.
|
|
209
|
+
- No keychain or vault state is touched. F-1 vault is untouched.
|
|
210
|
+
- No restart-requested.json or other state-file shape changes (file
|
|
211
|
+
shape additions are deferred to a follow-up per §2 above).
|
|
212
|
+
- The F-6 surface is currently uncalled in production code (no
|
|
213
|
+
Remediator instance calls `registerRemediator` on main yet). Revert
|
|
214
|
+
is safe at any point before the wrapper-PR series ships.
|
|
215
|
+
|
|
216
|
+
The A15 7-day age rule is what bounds the rollback risk for downstream
|
|
217
|
+
wrappers: a wrapper that depends on F-6 must wait until F-6 has been
|
|
218
|
+
on main + auto-updated for 7 days before merging, so a revert window
|
|
219
|
+
of < 7 days has zero downstream consumers to break.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Spec anchor
|
|
224
|
+
|
|
225
|
+
This change implements:
|
|
226
|
+
- **v2 §A15** (partial-upgrade window — supervisor handshake lag rule),
|
|
227
|
+
- **v2 carried-forward §Supervisor coordination** (HMAC-signed
|
|
228
|
+
restart-requested),
|
|
229
|
+
- **v2 §A20** (key segregation — the supervisor verifies HMACs against
|
|
230
|
+
the capability-context leaf, not the legacy `~/.instar/agent.key`),
|
|
231
|
+
- **v3 §3** (state-file taxonomy — `supervisor-handshake.json`,
|
|
232
|
+
`restart-requested.json` co-ownership),
|
|
233
|
+
- **v3 §9** (Tier-2 sequencing — F-6 unblocks W-2..W-4 after the 7-day
|
|
234
|
+
age requirement).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Side-Effects Review — F-7: PostUpdateMigrator atomic-step + announceOnce primitives (Tier-2)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `f7-post-update-migrator-atomic-step`
|
|
4
|
+
**Date:** `2026-05-13`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships F-7 from the Self-Healing Remediator v2 spec (`docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` §R1 Upgrade invariants + §A35 backup/sync wiring + §A50 hook-shape corrections + §A57 Tier-2 phase manifest).
|
|
11
|
+
|
|
12
|
+
Two new primitives plus the A35 hook-shape changes:
|
|
13
|
+
|
|
14
|
+
1. **`MigratorStepEngine`** — atomic, idempotent, run-once-per-version migration steps. Steps are registered by name + version. The engine writes a ledger at `<stateDir>/migrator-steps-completed.json` keyed by `<version>:<step-name>`. A step failure records `outcome: 'failed'` and does NOT roll back prior steps or block subsequent steps — each step is self-contained.
|
|
15
|
+
|
|
16
|
+
2. **`AnnouncementManager`** — `announceOnce(id, message, channel)` primitive. Emits a message exactly once per id, then never again. Ledger at `<stateDir>/announcements-shown.json`. Used for surfacing migration completions and structural-change notices without spamming the user across restarts.
|
|
17
|
+
|
|
18
|
+
3. **A35 hook-shape — `GitStateManager.DEFAULT_GITIGNORE` extension.** Const literal now contains five remediation runtime path globs (`remediation/system-reviewer-state-*.json`, `remediation/inbox-*.jsonl`, `remediation/audit-projection-*.jsonl`, `remediation/cross-process-attempts-*.jsonl`, `remediation/llm-raw-*.jsonl`). Exported as `REMEDIATION_GITIGNORE_ENTRIES` for the F-7 atomic step to use as the source-of-truth list.
|
|
19
|
+
|
|
20
|
+
4. **A35 hook-shape — `BackupManager` exclusion list with feature-flag gate.** New const `REMEDIATION_EXCLUDED_PATH_PREFIXES` (exported) — the five remediation path prefixes. New optional constructor arg `isRemediationEnabled?: () => boolean` parallels the existing `isIntegratedBeingEnabled` gate. When the gate is ON, the prefixes drop any user-added `includeFiles` entry that begins with a remediation prefix. When OFF (or absent), the prefixes are inactive — strictly additive, no back-compat break.
|
|
21
|
+
|
|
22
|
+
Files touched:
|
|
23
|
+
- `src/core/MigratorStepEngine.ts` (add)
|
|
24
|
+
- `src/core/PostUpdateMigrator.ts` (modify — add `registerStep` + `runPendingSteps` + lazy engine field; existing constructor + all 15 existing migration methods unchanged)
|
|
25
|
+
- `src/core/GitStateManager.ts` (modify — extend `DEFAULT_GITIGNORE` const literal; export `REMEDIATION_GITIGNORE_ENTRIES`)
|
|
26
|
+
- `src/core/BackupManager.ts` (modify — add `REMEDIATION_EXCLUDED_PATH_PREFIXES` const + export; add 5th optional constructor arg `isRemediationEnabled`; gate logic in `resolveIncludedFiles`)
|
|
27
|
+
- `tests/unit/PostUpdateMigrator-atomicStep.test.ts` (add — 8 cases)
|
|
28
|
+
- `tests/unit/AnnouncementManager.test.ts` (add — 5 cases)
|
|
29
|
+
- `tests/unit/PostUpdateMigrator-a35-remediationPaths.test.ts` (add — 5 cases)
|
|
30
|
+
- `upgrades/NEXT.md` (modify — preserves all existing entries; adds A35 + announceOnce entries)
|
|
31
|
+
|
|
32
|
+
## Decision-point inventory
|
|
33
|
+
|
|
34
|
+
- `PostUpdateMigrator.registerStep()` — **add** — delegates to lazy `MigratorStepEngine`. Existing 15 `migrate*` methods + `migrate()` orchestrator untouched.
|
|
35
|
+
- `PostUpdateMigrator.runPendingSteps()` — **add** — async; returns `{ steps: [{name, outcome, details}] }`. Never throws.
|
|
36
|
+
- `MigratorStepEngine.registerStep()` — rejects duplicate names, missing version/run.
|
|
37
|
+
- `MigratorStepEngine.runPendingSteps()` — three skip reasons: `already-recorded:<outcome>`, `future-version:<step.version> > <toVersion>`, and explicit `'skipped'` from the step itself. Failed steps record `outcome: 'failed'` and DO NOT retry (operator must clear the ledger entry to retry — surfaces the failure in the report).
|
|
38
|
+
- `MigratorStepEngine.writeLedger()` — atomic temp-file → fsync → rename, `0600` perms.
|
|
39
|
+
- `AnnouncementManager.announceOnce()` — three-input validation (id, message, channel); records BEFORE sink-invoke so a flaky sink cannot cause duplicate emission. Sink throws are swallowed (ledger entry stands).
|
|
40
|
+
- `GitStateManager.DEFAULT_GITIGNORE` const literal — **modify** — append remediation block. Backwards-compatible: previously private const is now exported (no internal call sites broken).
|
|
41
|
+
- `BackupManager` constructor — **modify** — append 5th optional argument. All four existing call sites pass ≤4 args and continue to work.
|
|
42
|
+
- `BackupManager.resolveIncludedFiles()` — **modify** — adds gated exclusion loop. When gate is OFF (the default — every existing caller), behavior is identical to pre-PR.
|
|
43
|
+
|
|
44
|
+
No new HTTP routes. No new daemon processes. No new file types beyond two JSON ledgers in `<stateDir>`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 1. Over-block
|
|
49
|
+
|
|
50
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
51
|
+
|
|
52
|
+
- **A future-version step is silently skipped.** Intended. The ledger is NOT written when the step's version > `toVersion`, so the next upgrade boundary that crosses the threshold runs the step.
|
|
53
|
+
- **A failed step is NOT auto-retried on the next `runPendingSteps()` invocation.** This could be seen as over-blocking — a transient failure (disk full, network blip) gets recorded as `failed` and stays that way. Trade-off accepted because the alternative (unbounded retry on every update) lets a broken step DoS every release. Operator-facing mitigation: the failed entry surfaces in the migration report; clearing the ledger key allows retry.
|
|
54
|
+
- **The `BackupManager` remediation-prefix gate, when ON, drops includeFiles entries whose paths begin with a remediation prefix.** A user who manually adds `.instar/remediation/system-reviewer-state-mine.json` to `config.backup.includeFiles` will see it silently dropped. Mitigated by: (a) the gate defaults to OFF until remediation is enabled; (b) the prefixes are documented in code + spec; (c) this is the explicit spec requirement (A14 "Backed up? no" rows).
|
|
55
|
+
|
|
56
|
+
No unintended over-blocks.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. Under-block
|
|
61
|
+
|
|
62
|
+
**What failure modes does this still miss?**
|
|
63
|
+
|
|
64
|
+
- **No cross-machine coordination of step execution.** Two machines updating to the same version in parallel each run their own steps independently. The ledger is per-machine. This is the correct model for per-machine state (gitignore lives in `<stateDir>/.gitignore` which is per-machine via the `.instar` git repo), but a future cross-machine migration would need additional coordination.
|
|
65
|
+
- **Step `run()` is not deadline-enforced.** A hung step could block subsequent steps. The engine relies on step authors to be well-behaved. Real-world steps are file I/O against `<stateDir>` and complete in <50ms; a step that calls external APIs should add its own timeout. Worst case: the migrator hangs, the next update boundary sees the same situation; the user can `kill` the process and the ledger remains consistent.
|
|
66
|
+
- **The `announceOnce` sink does not retry.** If a sink fails (e.g. Telegram offline), the announcement is recorded as shown but never delivered. This is the deliberate signal-vs-authority separation: the brittle sink layer does not have authority over the ledger. A future enhancement could add a separate delivery queue; F-7 keeps the primitive minimal.
|
|
67
|
+
- **Ledger corruption is treated as empty.** If `migrator-steps-completed.json` becomes corrupt (truncated, bad JSON), the engine falls back to an empty ledger. This is intentional — the alternative (refuse to migrate) would be worse. Cost: a corrupt ledger causes one round of step re-runs; each step is idempotent by spec.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Level-of-abstraction fit
|
|
72
|
+
|
|
73
|
+
**Is this at the right layer?**
|
|
74
|
+
|
|
75
|
+
Yes. The two primitives sit at the `src/core/` layer — the same layer as `PostUpdateMigrator`, `GitStateManager`, `BackupManager`. They compose on top of `node:fs` only; no dependency on the Remediator (Tier-2 surfaces will register their own steps via `migrator.registerStep(...)` after import). The A35 const-literal extensions sit where the const lives — `GitStateManager` for the gitignore, `BackupManager` for the exclusion prefixes. No new plugin/register API was introduced per A50's explicit ruling.
|
|
76
|
+
|
|
77
|
+
The `PostUpdateMigrator` class is the right owner for the public-facing `registerStep`/`runPendingSteps` methods even though the engine logic lives in `MigratorStepEngine` — the existing class is already the documented entry point for "thing that runs on update boundary". Tier-2/Tier-3 surfaces (S-2, S-3) will reach for `PostUpdateMigrator` first.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 4. Signal-vs-authority compliance
|
|
82
|
+
|
|
83
|
+
The brittle/low-context layers (file I/O ledger reads, sink invocations) are signal layers. The higher-context layer (the `runPendingSteps` orchestrator) is the authority. Specifically:
|
|
84
|
+
|
|
85
|
+
- A corrupt ledger does NOT block migration — the ledger is treated as a signal ("here's what we believe happened"), and the engine recovers by treating it as empty. Authority over "should this step run" lives in the in-memory step list + the version comparison.
|
|
86
|
+
- A failing sink does NOT block `announceOnce` from recording the announcement — sink delivery is signal; the ledger record is authority over "have we already attempted to surface this".
|
|
87
|
+
- A failing step does NOT block other steps — the per-step try/catch isolates signals; the engine retains authority over the run sequence.
|
|
88
|
+
|
|
89
|
+
This matches the §A50 / signal-vs-authority principle in MEMORY.md.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 5. Interactions with existing surfaces
|
|
94
|
+
|
|
95
|
+
- **`PostUpdateMigrator.migrate()` is untouched.** Existing callers (`src/update/UpdateService.ts`, `src/commands/init.ts`, etc.) continue to work — they call `migrate()` and never touch the new methods.
|
|
96
|
+
- **`GitStateManager.init()`** — writes `DEFAULT_GITIGNORE` only when no `.gitignore` exists. Existing repos with `.gitignore` already on disk are unaffected. New repos get the remediation entries pre-included.
|
|
97
|
+
- **`BackupManager` callers** — 14 existing call sites pass at most 4 constructor args. The new 5th arg is optional + defaults to no-op behavior. Verified by running the full `backup-manager.test.ts` (44 cases) and `BackupManager-sharedState.test.ts` (3 cases) — all pass unchanged.
|
|
98
|
+
- **`upgrades/NEXT.md`** — only appended to (preserving the W-1, F-8, F-3, F-4, F-1..F-2 entries already on main).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 6. Rollback cost
|
|
103
|
+
|
|
104
|
+
Low. The two new modules (`MigratorStepEngine.ts`, the new methods on `PostUpdateMigrator.ts`) are dormant unless something calls `registerStep` — and nothing calls it in this PR. Tier-2 surfaces will be the first consumers. Reverting requires:
|
|
105
|
+
|
|
106
|
+
1. `git revert` the PR. No data migration. No format changes. Existing `migrate()` behavior is untouched.
|
|
107
|
+
2. If any agent has already created `migrator-steps-completed.json` or `announcements-shown.json` files, they become unused leftovers — no harm. They can be deleted at leisure.
|
|
108
|
+
|
|
109
|
+
The A35 const-literal changes are inline edits to existing files; revert restores the prior literal. The `BackupManager`'s 5th constructor arg is optional and unused by every current caller, so removing it has zero blast radius.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Tests
|
|
114
|
+
|
|
115
|
+
- `tests/unit/PostUpdateMigrator-atomicStep.test.ts` — 8 cases covering: step runs once and records completion; step is skipped on subsequent runs; failed step records failure and doesn't block other steps; future-version step skipped without ledger entry; state persists across instances; semver compare correctness.
|
|
116
|
+
- `tests/unit/AnnouncementManager.test.ts` — 5 cases covering: first-true / subsequent-false; independent ids; persistence across instances; sink-throw does not cause re-emission; input validation.
|
|
117
|
+
- `tests/unit/PostUpdateMigrator-a35-remediationPaths.test.ts` — 5 cases covering: `DEFAULT_GITIGNORE` contains all five remediation entries; `REMEDIATION_EXCLUDED_PATH_PREFIXES` exported correctly; gate ON drops remediation entries; gate OFF preserves them; absent callback = back-compat (gate OFF).
|
|
118
|
+
|
|
119
|
+
All 18 new tests pass. All 76 pre-existing `PostUpdateMigrator-*` tests pass unchanged. All 47 `BackupManager*` tests pass unchanged. TypeScript clean (`tsc --noEmit`).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Gate trace
|
|
124
|
+
|
|
125
|
+
- side-effects review: this document
|
|
126
|
+
- NEXT.md: amended, preserving every prior entry
|
|
127
|
+
- spec citation: `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` §R1 + §A35 + §A50 + §A57
|
|
128
|
+
- worktree: `/tmp/instar-f7-post-update-migrator` (branch `f7-post-update-migrator`)
|
|
129
|
+
- no bypass; no `--no-verify`
|