@scotthuang/agent-knock-knock 0.12.11 → 0.12.13
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/CHANGELOG.md +40 -0
- package/README.md +43 -30
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/callback-cli-adapter.d.ts +47 -0
- package/dist/src/callback-cli-adapter.js +156 -0
- package/dist/src/callback-cli-adapter.js.map +1 -0
- package/dist/src/callback-outbox-policy.d.ts +95 -0
- package/dist/src/callback-outbox-policy.js +149 -0
- package/dist/src/callback-outbox-policy.js.map +1 -0
- package/dist/src/callback-outbox-service.d.ts +247 -0
- package/dist/src/callback-outbox-service.js +1053 -0
- package/dist/src/callback-outbox-service.js.map +1 -0
- package/dist/src/callback-outbox-settlement.d.ts +54 -0
- package/dist/src/callback-outbox-settlement.js +241 -0
- package/dist/src/callback-outbox-settlement.js.map +1 -0
- package/dist/src/canonical-json.d.ts +1 -0
- package/dist/src/canonical-json.js +14 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/claude-lifecycle-compatibility.js +15 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +82 -1
- package/dist/src/claude-local-transcript-provider.js +873 -23
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-command-runtime.d.ts +14 -0
- package/dist/src/cli-command-runtime.js +164 -0
- package/dist/src/cli-command-runtime.js.map +1 -0
- package/dist/src/cli-core.d.ts +4 -42
- package/dist/src/cli-core.js +958 -28684
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/cli-runtime-context.d.ts +57 -0
- package/dist/src/cli-runtime-context.js +118 -0
- package/dist/src/cli-runtime-context.js.map +1 -0
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +18 -0
- package/dist/src/codex-local-session-provider.js +67 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-session-provider.js +1 -3
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/conversation-trace.d.ts +30 -0
- package/dist/src/conversation-trace.js +185 -0
- package/dist/src/conversation-trace.js.map +1 -0
- package/dist/src/deferred-foreground-application-service.d.ts +84 -0
- package/dist/src/deferred-foreground-application-service.js +742 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js +795 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-boundary.d.ts +61 -0
- package/dist/src/deferred-foreground-boundary.js +2 -0
- package/dist/src/deferred-foreground-boundary.js.map +1 -0
- package/dist/src/deferred-foreground-capability.d.ts +4 -0
- package/dist/src/deferred-foreground-capability.js +184 -0
- package/dist/src/deferred-foreground-capability.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
- package/dist/src/deferred-foreground-preparation-service.js +179 -0
- package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
- package/dist/src/deferred-foreground-recovery-service.js +91 -0
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
- package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
- package/dist/src/deferred-foreground-transfer-policy.js +10 -0
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
- package/dist/src/deferred-foreground-transfer.js +46 -105
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +9 -0
- package/dist/src/doctor-capabilities.js +22 -0
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/durable-json-file.d.ts +22 -0
- package/dist/src/durable-json-file.js +127 -0
- package/dist/src/durable-json-file.js.map +1 -0
- package/dist/src/file-lock-cli-adapter.d.ts +43 -0
- package/dist/src/file-lock-cli-adapter.js +150 -0
- package/dist/src/file-lock-cli-adapter.js.map +1 -0
- package/dist/src/herdr-terminal-control-provider.js +1 -3
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/install-doctor-command-adapter.d.ts +17 -0
- package/dist/src/install-doctor-command-adapter.js +225 -0
- package/dist/src/install-doctor-command-adapter.js.map +1 -0
- package/dist/src/live-lifecycle-evidence.js +76 -46
- package/dist/src/live-lifecycle-evidence.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +74 -50
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/managed-session.d.ts +2 -0
- package/dist/src/managed-session.js +24 -0
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/managed-turn-recovery-service.d.ts +86 -0
- package/dist/src/managed-turn-recovery-service.js +145 -0
- package/dist/src/managed-turn-recovery-service.js.map +1 -0
- package/dist/src/mutation-transaction.d.ts +48 -0
- package/dist/src/mutation-transaction.js +73 -0
- package/dist/src/mutation-transaction.js.map +1 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
- package/dist/src/native-thread-lifecycle-query-service.js +297 -0
- package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
- package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
- package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
- package/dist/src/native-thread-resume-snapshot.js +2 -143
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/native-thread-transition-application.d.ts +147 -0
- package/dist/src/native-thread-transition-application.js +974 -0
- package/dist/src/native-thread-transition-application.js.map +1 -0
- package/dist/src/native-thread-transition-policy.d.ts +127 -0
- package/dist/src/native-thread-transition-policy.js +154 -0
- package/dist/src/native-thread-transition-policy.js.map +1 -0
- package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
- package/dist/src/native-thread-transition-resource-adapter.js +44 -0
- package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
- package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
- package/dist/src/native-thread-transition-settlement-service.js +134 -0
- package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
- package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
- package/dist/src/native-thread-transition-verification-adapter.js +451 -0
- package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +69 -0
- package/dist/src/openclaw-callback-transport.js +583 -0
- package/dist/src/openclaw-callback-transport.js.map +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.js +423 -0
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
- package/dist/src/openclaw-plugin-command-adapter.js +1868 -0
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +356 -81
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +358 -0
- package/dist/src/openclaw-plugin-schemas.js +366 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -0
- package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
- package/dist/src/openclaw-plugin-supervisor.js +123 -0
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
- package/dist/src/openclaw-plugin.js +7 -2212
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/protocol.d.ts +5 -0
- package/dist/src/protocol.js +59 -3
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/session-store.d.ts +1 -0
- package/dist/src/session-store.js +84 -106
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +5 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
- package/dist/src/terminal-acceptance-application-service.js +63 -0
- package/dist/src/terminal-acceptance-application-service.js.map +1 -0
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-action-projection.d.ts +309 -0
- package/dist/src/terminal-action-projection.js +376 -0
- package/dist/src/terminal-action-projection.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +3 -0
- package/dist/src/terminal-agent-bridge.js +29 -8
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-authority-policy.d.ts +141 -0
- package/dist/src/terminal-authority-policy.js +367 -0
- package/dist/src/terminal-authority-policy.js.map +1 -0
- package/dist/src/terminal-binding-authority.d.ts +118 -0
- package/dist/src/terminal-binding-authority.js +245 -0
- package/dist/src/terminal-binding-authority.js.map +1 -0
- package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
- package/dist/src/terminal-binding-reconciliation-service.js +73 -0
- package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-command-cli-adapter.d.ts +380 -0
- package/dist/src/terminal-command-cli-adapter.js +3288 -0
- package/dist/src/terminal-command-cli-adapter.js.map +1 -0
- package/dist/src/terminal-control-ref.d.ts +4 -0
- package/dist/src/terminal-control-ref.js +11 -8
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
- package/dist/src/terminal-delegate-cli-adapter.js +242 -0
- package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-abort.d.ts +22 -0
- package/dist/src/terminal-dispatch-abort.js +30 -0
- package/dist/src/terminal-dispatch-abort.js.map +1 -0
- package/dist/src/terminal-dispatch-application.d.ts +115 -0
- package/dist/src/terminal-dispatch-application.js +478 -0
- package/dist/src/terminal-dispatch-application.js.map +1 -0
- package/dist/src/terminal-dispatch-capability.d.ts +63 -0
- package/dist/src/terminal-dispatch-capability.js +133 -0
- package/dist/src/terminal-dispatch-capability.js.map +1 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-composition.d.ts +79 -0
- package/dist/src/terminal-dispatch-composition.js +2 -0
- package/dist/src/terminal-dispatch-composition.js.map +1 -0
- package/dist/src/terminal-dispatch-execution.d.ts +290 -0
- package/dist/src/terminal-dispatch-execution.js +916 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -0
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
- package/dist/src/terminal-dispatch-policy.d.ts +93 -0
- package/dist/src/terminal-dispatch-policy.js +122 -0
- package/dist/src/terminal-dispatch-policy.js.map +1 -0
- package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
- package/dist/src/terminal-dispatch-presenter.js +258 -0
- package/dist/src/terminal-dispatch-presenter.js.map +1 -0
- package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
- package/dist/src/terminal-dispatch-receipt.js +314 -0
- package/dist/src/terminal-dispatch-receipt.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
- package/dist/src/terminal-dispatch-recovery-service.js +374 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +483 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-application-service.d.ts +7 -0
- package/dist/src/terminal-handoff-application-service.js +18 -0
- package/dist/src/terminal-handoff-application-service.js.map +1 -0
- package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
- package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
- package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-facts.d.ts +12 -0
- package/dist/src/terminal-handoff-facts.js +16 -0
- package/dist/src/terminal-handoff-facts.js.map +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-identity-authority-service.d.ts +48 -0
- package/dist/src/terminal-identity-authority-service.js +45 -0
- package/dist/src/terminal-identity-authority-service.js.map +1 -0
- package/dist/src/terminal-list-cli-adapter.d.ts +274 -0
- package/dist/src/terminal-list-cli-adapter.js +2444 -0
- package/dist/src/terminal-list-cli-adapter.js.map +1 -0
- package/dist/src/terminal-list-renderer.d.ts +34 -0
- package/dist/src/terminal-list-renderer.js +643 -0
- package/dist/src/terminal-list-renderer.js.map +1 -0
- package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
- package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-application-service.d.ts +275 -0
- package/dist/src/terminal-monitor-application-service.js +982 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -0
- package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
- package/dist/src/terminal-monitor-cli-adapter.js +442 -0
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
- package/dist/src/terminal-monitor-decision-policy.js +246 -0
- package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
- package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
- package/dist/src/terminal-monitor-launch-plan.js +94 -0
- package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
- package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
- package/dist/src/terminal-monitor-ownership-policy.js +64 -0
- package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
- package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
- package/dist/src/terminal-monitor-poll-policy.js +67 -0
- package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js +1492 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
- package/dist/src/terminal-process-facts.d.ts +2 -0
- package/dist/src/terminal-process-facts.js +19 -0
- package/dist/src/terminal-process-facts.js.map +1 -0
- package/dist/src/terminal-process-source.d.ts +6 -4
- package/dist/src/terminal-process-source.js +25 -20
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
- package/dist/src/terminal-runtime-cli-adapter.js +488 -0
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
- package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
- package/dist/src/terminal-scoped-approval-authority.js +281 -0
- package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
- package/dist/src/terminal-selector-projection-service.d.ts +26 -0
- package/dist/src/terminal-selector-projection-service.js +81 -0
- package/dist/src/terminal-selector-projection-service.js.map +1 -0
- package/dist/src/terminal-status-cli-adapter.d.ts +126 -0
- package/dist/src/terminal-status-cli-adapter.js +577 -0
- package/dist/src/terminal-status-cli-adapter.js.map +1 -0
- package/dist/src/terminal-status-facts.d.ts +56 -0
- package/dist/src/terminal-status-facts.js +199 -0
- package/dist/src/terminal-status-facts.js.map +1 -0
- package/dist/src/terminal-submission-acceptance.d.ts +62 -145
- package/dist/src/terminal-submission-acceptance.js +555 -222
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-facts.d.ts +123 -0
- package/dist/src/terminal-submission-facts.js +205 -0
- package/dist/src/terminal-submission-facts.js.map +1 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/dist/src/value-guards.d.ts +4 -0
- package/dist/src/value-guards.js +12 -0
- package/dist/src/value-guards.js.map +1 -0
- package/dist/src/verified-dead-agent-policy.d.ts +192 -0
- package/dist/src/verified-dead-agent-policy.js +262 -0
- package/dist/src/verified-dead-agent-policy.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +3 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +58 -40
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertRealDirectory, atomicSaveJsonFile, isNodeError, readJsonFileNoFollow } from "./durable-json-file.js";
|
|
5
|
+
import { initialClaudeHumanStartedActiveTaskCheckpoint, validateClaudeHumanStartedActiveTaskCheckpoint, validateClaudeHumanStartedActiveTaskAnchor } from "./claude-local-transcript-provider.js";
|
|
6
|
+
import { validateCodexHumanStartedActiveTaskAnchor } from "./terminal-submission-acceptance.js";
|
|
7
|
+
import { assertStoreReadable, ensureDir, STORE_TERMINAL_WATCHES_DIRECTORY, withStoreWriterLease } from "./store.js";
|
|
8
|
+
import { terminalEndpointIdentityFromEvidence, terminalRouteKeyFromEvidence } from "./terminal-control-ref.js";
|
|
9
|
+
import { isRecord } from "./value-guards.js";
|
|
10
|
+
export const TERMINAL_WATCH_SCHEMA = "agent-knock-knock/terminal-watch";
|
|
11
|
+
export const TERMINAL_WATCH_VERSION = 1;
|
|
12
|
+
export const TERMINAL_WATCHES_DIRECTORY = STORE_TERMINAL_WATCHES_DIRECTORY;
|
|
13
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
14
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
15
|
+
const TERMINAL_WATCH_TERMINAL_STATUSES = [
|
|
16
|
+
"completed",
|
|
17
|
+
"failed",
|
|
18
|
+
"timed_out",
|
|
19
|
+
"invalidated",
|
|
20
|
+
"cancelled"
|
|
21
|
+
];
|
|
22
|
+
const TERMINAL_WATCH_STATUSES = [
|
|
23
|
+
"active",
|
|
24
|
+
...TERMINAL_WATCH_TERMINAL_STATUSES
|
|
25
|
+
];
|
|
26
|
+
const TERMINAL_WATCH_NOTIFICATION_KINDS = [
|
|
27
|
+
"approval",
|
|
28
|
+
...TERMINAL_WATCH_TERMINAL_STATUSES
|
|
29
|
+
];
|
|
30
|
+
const TERMINAL_WATCH_NOTIFICATION_STATUSES = [
|
|
31
|
+
"pending",
|
|
32
|
+
"delivering",
|
|
33
|
+
"failed",
|
|
34
|
+
"delivered",
|
|
35
|
+
"superseded"
|
|
36
|
+
];
|
|
37
|
+
export function initialTerminalWatchObservationCheckpoint(anchor) {
|
|
38
|
+
if (anchor.schema ===
|
|
39
|
+
"agent-knock-knock/claude-human-started-active-task-anchor") {
|
|
40
|
+
return initialClaudeHumanStartedActiveTaskCheckpoint(anchor);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
safe_resume_offset_bytes: validateCodexHumanStartedActiveTaskAnchor(anchor)
|
|
44
|
+
.observed_end_offset_bytes
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export class TerminalWatchConflictError extends Error {
|
|
48
|
+
code = "AKK_TERMINAL_WATCH_CONFLICT";
|
|
49
|
+
watchId;
|
|
50
|
+
expectedRevision;
|
|
51
|
+
actualRevision;
|
|
52
|
+
constructor(watchId, expectedRevision, actualRevision, detail) {
|
|
53
|
+
super(`terminal Watch ${watchId} changed concurrently` +
|
|
54
|
+
` (expected revision ${String(expectedRevision)}, actual ` +
|
|
55
|
+
`${String(actualRevision)})` +
|
|
56
|
+
(detail ? `: ${detail}` : ""));
|
|
57
|
+
this.name = "TerminalWatchConflictError";
|
|
58
|
+
this.watchId = watchId;
|
|
59
|
+
this.expectedRevision = expectedRevision;
|
|
60
|
+
this.actualRevision = actualRevision;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function terminalWatchesDir(storeDir) {
|
|
64
|
+
return path.join(storeDir, TERMINAL_WATCHES_DIRECTORY);
|
|
65
|
+
}
|
|
66
|
+
export function pathsForTerminalWatch(watchId, storeDir) {
|
|
67
|
+
assertRecordId(watchId, "terminal Watch id");
|
|
68
|
+
const root = terminalWatchesDir(storeDir);
|
|
69
|
+
const statePath = path.join(root, `${watchId}.json`);
|
|
70
|
+
assertContained(statePath, root, "terminal Watch state");
|
|
71
|
+
return { root, statePath, lockPath: `${statePath}.lock` };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Acquire the Store writer lease before the per-Watch state lock. This keeps
|
|
75
|
+
* the repository compatible with the canonical `writer -> state` lock order.
|
|
76
|
+
*/
|
|
77
|
+
export function withTerminalWatchLock(storeDir, watchId, locks, operation) {
|
|
78
|
+
const paths = pathsForTerminalWatch(watchId, storeDir);
|
|
79
|
+
return withStoreWriterLease(storeDir, () => {
|
|
80
|
+
ensureDir(paths.root);
|
|
81
|
+
assertOwnerPrivateDirectory(paths.root, "terminal Watch root");
|
|
82
|
+
const release = locks.acquire(paths.lockPath);
|
|
83
|
+
try {
|
|
84
|
+
return operation();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
release();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function createTerminalWatchStore(storeDir, locks) {
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
list: () => listTerminalWatches(storeDir),
|
|
94
|
+
scanForReconciliation: () => scanTerminalWatchesForReconciliation(storeDir),
|
|
95
|
+
load: (watchId) => loadTerminalWatch(storeDir, watchId),
|
|
96
|
+
save: (watch, options) => saveTerminalWatch(storeDir, watch, options),
|
|
97
|
+
withWatchLock: (watchId, operation) => withTerminalWatchLock(storeDir, watchId, locks, operation)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function terminalWatchNotificationId(watchId, kind, evidenceFingerprint) {
|
|
101
|
+
assertRecordId(watchId, "terminal Watch id");
|
|
102
|
+
assertNotificationKind(kind);
|
|
103
|
+
assertSha256(evidenceFingerprint, "notification evidence fingerprint");
|
|
104
|
+
const digest = createHash("sha256")
|
|
105
|
+
.update(JSON.stringify({
|
|
106
|
+
schema: "agent-knock-knock/terminal-watch-notification",
|
|
107
|
+
version: 1,
|
|
108
|
+
watch_id: watchId,
|
|
109
|
+
kind,
|
|
110
|
+
evidence_fingerprint: evidenceFingerprint
|
|
111
|
+
}))
|
|
112
|
+
.digest("hex");
|
|
113
|
+
return `terminal-watch-notification-${digest}`;
|
|
114
|
+
}
|
|
115
|
+
export function terminalWatchNotificationIdempotencyKey(watchId, notificationId) {
|
|
116
|
+
assertRecordId(watchId, "terminal Watch id");
|
|
117
|
+
assertRecordId(notificationId, "terminal Watch notification id");
|
|
118
|
+
return `agent-knock-knock:terminal-watch:${watchId}:${notificationId}`;
|
|
119
|
+
}
|
|
120
|
+
export function terminalWatchIdentityFingerprint(watch) {
|
|
121
|
+
const endpoint = terminalEndpointIdentityFromEvidence(watch.terminal.terminal_endpoint);
|
|
122
|
+
if (!endpoint) {
|
|
123
|
+
throw new Error("terminal Watch endpoint identity is invalid");
|
|
124
|
+
}
|
|
125
|
+
return createHash("sha256")
|
|
126
|
+
.update(JSON.stringify({
|
|
127
|
+
version: 1,
|
|
128
|
+
agent: watch.agent,
|
|
129
|
+
terminal_id: watch.terminal.terminal_id,
|
|
130
|
+
endpoint,
|
|
131
|
+
process_anchor_pid: watch.terminal.terminal_endpoint.process_anchor_pid,
|
|
132
|
+
workspace: watch.terminal.workspace,
|
|
133
|
+
binding_token: watch.terminal.binding_token,
|
|
134
|
+
anchor_fingerprint: watch.anchor.anchor_fingerprint
|
|
135
|
+
}))
|
|
136
|
+
.digest("hex");
|
|
137
|
+
}
|
|
138
|
+
export function terminalWatchRevision(watch) {
|
|
139
|
+
if (!isPositiveSafeInteger(watch.revision)) {
|
|
140
|
+
throw new Error(`terminal Watch ${watch.watch_id} has no valid revision`);
|
|
141
|
+
}
|
|
142
|
+
return watch.revision;
|
|
143
|
+
}
|
|
144
|
+
const IGNORE_VALUE = () => { };
|
|
145
|
+
const POSITIVE_INTEGER = (value, label) => {
|
|
146
|
+
if (!isPositiveSafeInteger(value)) {
|
|
147
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const NON_NEGATIVE_INTEGER = (value, label) => {
|
|
151
|
+
if (!isNonNegativeSafeInteger(value)) {
|
|
152
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const ARRAY_VALUE = (value, label) => {
|
|
156
|
+
if (!Array.isArray(value))
|
|
157
|
+
throw new Error(`${label} must be an array`);
|
|
158
|
+
};
|
|
159
|
+
const ABSOLUTE_PATH = (value, label) => {
|
|
160
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) {
|
|
161
|
+
throw new Error(`${label} must be absolute`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const NULLABLE_ENDPOINT_STRING = optionalGuard((value, label) => {
|
|
165
|
+
if (value !== null &&
|
|
166
|
+
(typeof value !== "string" || value.includes("\0"))) {
|
|
167
|
+
throw new Error(`${label} is invalid`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const WATCH_FIELDS = {
|
|
171
|
+
schema: literalGuard(TERMINAL_WATCH_SCHEMA),
|
|
172
|
+
version: literalGuard(TERMINAL_WATCH_VERSION),
|
|
173
|
+
watch_id: assertRecordId,
|
|
174
|
+
revision: optionalGuard(POSITIVE_INTEGER),
|
|
175
|
+
agent: oneOfGuard(["codex", "claude"]),
|
|
176
|
+
terminal: assertTerminalIdentity,
|
|
177
|
+
anchor: IGNORE_VALUE,
|
|
178
|
+
observation_checkpoint: IGNORE_VALUE,
|
|
179
|
+
openclaw_session: assertNonEmptyString,
|
|
180
|
+
openclaw_bin: assertNonEmptyString,
|
|
181
|
+
created_at: assertTimestamp,
|
|
182
|
+
deadline_at: assertTimestamp,
|
|
183
|
+
updated_at: assertTimestamp,
|
|
184
|
+
status: oneOfGuard(TERMINAL_WATCH_STATUSES),
|
|
185
|
+
last_activity_at: assertTimestamp,
|
|
186
|
+
settlement: IGNORE_VALUE,
|
|
187
|
+
notification_outbox: ARRAY_VALUE
|
|
188
|
+
};
|
|
189
|
+
const TERMINAL_IDENTITY_FIELDS = {
|
|
190
|
+
terminal_id: assertNonEmptyString,
|
|
191
|
+
terminal_endpoint: assertTerminalEndpoint,
|
|
192
|
+
workspace: ABSOLUTE_PATH,
|
|
193
|
+
binding_token: assertSha256
|
|
194
|
+
};
|
|
195
|
+
const TERMINAL_ENDPOINT_FIELDS = {
|
|
196
|
+
schema: literalGuard("agent-knock-knock/terminal-endpoint"),
|
|
197
|
+
version: literalGuard(1),
|
|
198
|
+
kind: IGNORE_VALUE,
|
|
199
|
+
endpoint_key: IGNORE_VALUE,
|
|
200
|
+
resource_key: IGNORE_VALUE,
|
|
201
|
+
route_key: IGNORE_VALUE,
|
|
202
|
+
process_anchor_pid: POSITIVE_INTEGER,
|
|
203
|
+
target: NULLABLE_ENDPOINT_STRING,
|
|
204
|
+
socket_path: NULLABLE_ENDPOINT_STRING,
|
|
205
|
+
pane_pid: optionalGuard(nullableGuard(POSITIVE_INTEGER)),
|
|
206
|
+
server_socket_path: NULLABLE_ENDPOINT_STRING,
|
|
207
|
+
pane_id: NULLABLE_ENDPOINT_STRING,
|
|
208
|
+
session_name: NULLABLE_ENDPOINT_STRING,
|
|
209
|
+
session_dir: NULLABLE_ENDPOINT_STRING,
|
|
210
|
+
workspace_id: NULLABLE_ENDPOINT_STRING,
|
|
211
|
+
tab_id: NULLABLE_ENDPOINT_STRING,
|
|
212
|
+
terminal_id: NULLABLE_ENDPOINT_STRING,
|
|
213
|
+
current_path: NULLABLE_ENDPOINT_STRING
|
|
214
|
+
};
|
|
215
|
+
const SETTLEMENT_FIELDS = {
|
|
216
|
+
kind: oneOfGuard(TERMINAL_WATCH_TERMINAL_STATUSES),
|
|
217
|
+
evidence_fingerprint: assertSha256,
|
|
218
|
+
observed_at: assertTimestamp,
|
|
219
|
+
reason_code: optionalGuard(assertReasonCode),
|
|
220
|
+
completion_text: optionalGuard(assertCompletionText),
|
|
221
|
+
completion_id: optionalGuard(assertNonEmptyString),
|
|
222
|
+
completion_timestamp: optionalGuard(assertTimestamp)
|
|
223
|
+
};
|
|
224
|
+
const NOTIFICATION_RECEIPT_FIELDS = {
|
|
225
|
+
last_attempt_at: optionalGuard(assertTimestamp),
|
|
226
|
+
attempt_id: optionalGuard(assertNonEmptyString),
|
|
227
|
+
attempt_lease_expires_at: optionalGuard(assertTimestamp),
|
|
228
|
+
failed_at: optionalGuard(assertTimestamp),
|
|
229
|
+
next_attempt_at: optionalGuard(assertTimestamp),
|
|
230
|
+
last_error_code: optionalGuard(assertReasonCode),
|
|
231
|
+
delivered_at: optionalGuard(assertTimestamp),
|
|
232
|
+
superseded_at: optionalGuard(assertTimestamp)
|
|
233
|
+
};
|
|
234
|
+
const NOTIFICATION_FIELDS = {
|
|
235
|
+
notification_id: assertNonEmptyString,
|
|
236
|
+
idempotency_key: assertNonEmptyString,
|
|
237
|
+
kind: oneOfGuard(TERMINAL_WATCH_NOTIFICATION_KINDS),
|
|
238
|
+
evidence_fingerprint: assertSha256,
|
|
239
|
+
reason_code: optionalGuard(assertReasonCode),
|
|
240
|
+
status: oneOfGuard(TERMINAL_WATCH_NOTIFICATION_STATUSES),
|
|
241
|
+
attempts: NON_NEGATIVE_INTEGER,
|
|
242
|
+
created_at: assertTimestamp,
|
|
243
|
+
...NOTIFICATION_RECEIPT_FIELDS
|
|
244
|
+
};
|
|
245
|
+
const NOTIFICATION_SHAPES = {
|
|
246
|
+
pending: [0, []],
|
|
247
|
+
delivering: [1, [
|
|
248
|
+
"last_attempt_at",
|
|
249
|
+
"attempt_id",
|
|
250
|
+
"attempt_lease_expires_at"
|
|
251
|
+
]],
|
|
252
|
+
failed: [1, [
|
|
253
|
+
"last_attempt_at",
|
|
254
|
+
"failed_at",
|
|
255
|
+
"next_attempt_at",
|
|
256
|
+
"last_error_code"
|
|
257
|
+
]],
|
|
258
|
+
delivered: [1, ["last_attempt_at", "delivered_at"]],
|
|
259
|
+
superseded: [0, ["superseded_at"]]
|
|
260
|
+
};
|
|
261
|
+
export function assertTerminalWatch(value, expectedWatchId, options = {}) {
|
|
262
|
+
assertStrictRecord(value, "terminal Watch", WATCH_FIELDS);
|
|
263
|
+
if (expectedWatchId !== undefined && value.watch_id !== expectedWatchId) {
|
|
264
|
+
throw new Error(`terminal Watch id ${String(value.watch_id)} does not match ${expectedWatchId}`);
|
|
265
|
+
}
|
|
266
|
+
if (value.revision === undefined && !options.allowMissingRevision) {
|
|
267
|
+
throw new Error("terminal Watch revision must be a positive safe integer");
|
|
268
|
+
}
|
|
269
|
+
const watch = value;
|
|
270
|
+
assertTerminalWatchAnchor(watch.anchor, watch.agent, watch.terminal);
|
|
271
|
+
assertObservationCheckpoint(watch.observation_checkpoint, watch.anchor);
|
|
272
|
+
const minimumCheckpointOffset = watch.anchor.schema ===
|
|
273
|
+
"agent-knock-knock/claude-human-started-active-task-anchor"
|
|
274
|
+
? watch.anchor.turn_start_offset_bytes
|
|
275
|
+
: watch.anchor.observed_end_offset_bytes;
|
|
276
|
+
if (watch.observation_checkpoint.safe_resume_offset_bytes <
|
|
277
|
+
minimumCheckpointOffset) {
|
|
278
|
+
throw new Error("terminal Watch observation checkpoint cannot predate its task anchor");
|
|
279
|
+
}
|
|
280
|
+
if (Date.parse(watch.deadline_at) <= Date.parse(watch.created_at) ||
|
|
281
|
+
Date.parse(watch.updated_at) < Date.parse(watch.created_at) ||
|
|
282
|
+
Date.parse(watch.last_activity_at) > Date.parse(watch.updated_at)) {
|
|
283
|
+
throw new Error("terminal Watch timestamps are not monotonic");
|
|
284
|
+
}
|
|
285
|
+
const processStartedAt = watch.anchor.schema ===
|
|
286
|
+
"agent-knock-knock/claude-human-started-active-task-anchor"
|
|
287
|
+
? watch.anchor.agent_started_at_ms
|
|
288
|
+
: undefined;
|
|
289
|
+
if (Date.parse(watch.anchor.captured_at) > Date.parse(watch.created_at) ||
|
|
290
|
+
(processStartedAt !== undefined &&
|
|
291
|
+
processStartedAt > Date.parse(watch.created_at))) {
|
|
292
|
+
throw new Error("terminal Watch cannot predate its exact observation anchor");
|
|
293
|
+
}
|
|
294
|
+
assertSettlement(watch.settlement, watch.status);
|
|
295
|
+
if (watch.settlement &&
|
|
296
|
+
(Date.parse(watch.settlement.observed_at) > Date.parse(watch.updated_at) ||
|
|
297
|
+
(watch.settlement.completion_timestamp !== undefined &&
|
|
298
|
+
Date.parse(watch.settlement.completion_timestamp) >
|
|
299
|
+
Date.parse(watch.updated_at)))) {
|
|
300
|
+
throw new Error("terminal Watch settlement cannot be newer than its state");
|
|
301
|
+
}
|
|
302
|
+
assertNotificationOutbox(watch);
|
|
303
|
+
}
|
|
304
|
+
export function saveTerminalWatch(storeDir, watch, options) {
|
|
305
|
+
assertTerminalWatch(watch, undefined, { allowMissingRevision: true });
|
|
306
|
+
assertExpectedRevision(options?.expectedRevision);
|
|
307
|
+
const paths = pathsForTerminalWatch(watch.watch_id, storeDir);
|
|
308
|
+
return withStoreWriterLease(storeDir, () => {
|
|
309
|
+
const current = tryLoadTerminalWatch(storeDir, watch.watch_id);
|
|
310
|
+
const actualRevision = current?.revision ?? null;
|
|
311
|
+
if (actualRevision !== options.expectedRevision) {
|
|
312
|
+
throw new TerminalWatchConflictError(watch.watch_id, options.expectedRevision, actualRevision);
|
|
313
|
+
}
|
|
314
|
+
if (watch.revision !== undefined &&
|
|
315
|
+
watch.revision !== (options.expectedRevision ?? 1)) {
|
|
316
|
+
throw new TerminalWatchConflictError(watch.watch_id, options.expectedRevision, actualRevision, `candidate carries revision ${watch.revision}`);
|
|
317
|
+
}
|
|
318
|
+
if (current) {
|
|
319
|
+
assertTerminalWatchAdvance(current, watch);
|
|
320
|
+
}
|
|
321
|
+
else if (watch.status !== "active" || watch.settlement !== undefined) {
|
|
322
|
+
throw new Error("a terminal Watch must be created active and unsettled");
|
|
323
|
+
}
|
|
324
|
+
const next = {
|
|
325
|
+
...watch,
|
|
326
|
+
revision: (actualRevision ?? 0) + 1
|
|
327
|
+
};
|
|
328
|
+
assertTerminalWatch(next, watch.watch_id);
|
|
329
|
+
atomicSaveTerminalWatch(paths.statePath, next);
|
|
330
|
+
return next;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
export function loadTerminalWatch(storeDir, watchId) {
|
|
334
|
+
assertStoreReadable(storeDir);
|
|
335
|
+
const paths = pathsForTerminalWatch(watchId, storeDir);
|
|
336
|
+
assertOwnerPrivateDirectory(paths.root, "terminal Watch root");
|
|
337
|
+
assertOwnerPrivateFile(paths.statePath, "terminal Watch state");
|
|
338
|
+
const value = readJsonFileNoFollow(paths.statePath, "terminal Watch state");
|
|
339
|
+
return decodeTerminalWatch(value, watchId);
|
|
340
|
+
}
|
|
341
|
+
export function tryLoadTerminalWatch(storeDir, watchId) {
|
|
342
|
+
try {
|
|
343
|
+
return loadTerminalWatch(storeDir, watchId);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (isNodeError(error, "ENOENT")) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export function listTerminalWatches(storeDir) {
|
|
353
|
+
const scan = scanTerminalWatchDirectory(storeDir, false);
|
|
354
|
+
return scan.watches;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Reconciliation isolates only malformed contents of an already-named JSON
|
|
358
|
+
* record. Directory shape, symlinks, unknown entries, permissions, and I/O
|
|
359
|
+
* failures remain global fail-closed boundaries.
|
|
360
|
+
*/
|
|
361
|
+
export function scanTerminalWatchesForReconciliation(storeDir) {
|
|
362
|
+
return scanTerminalWatchDirectory(storeDir, true);
|
|
363
|
+
}
|
|
364
|
+
function scanTerminalWatchDirectory(storeDir, isolateInvalidRecord) {
|
|
365
|
+
if (!fs.existsSync(storeDir)) {
|
|
366
|
+
return { watches: [], errors: [] };
|
|
367
|
+
}
|
|
368
|
+
assertStoreReadable(storeDir);
|
|
369
|
+
const root = terminalWatchesDir(storeDir);
|
|
370
|
+
if (!fs.existsSync(root)) {
|
|
371
|
+
return { watches: [], errors: [] };
|
|
372
|
+
}
|
|
373
|
+
assertOwnerPrivateDirectory(root, "terminal Watch root");
|
|
374
|
+
const watches = [];
|
|
375
|
+
const errors = [];
|
|
376
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
377
|
+
const entryPath = path.join(root, entry.name);
|
|
378
|
+
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
379
|
+
throw new Error(`terminal Watch root contains an invalid entry: ${entryPath}`);
|
|
380
|
+
}
|
|
381
|
+
if (isRecognizedWatchLockFile(entry.name) ||
|
|
382
|
+
isRecognizedWatchTemporaryFile(entry.name)) {
|
|
383
|
+
assertPrivateTransientEntry(entryPath);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!entry.name.endsWith(".json")) {
|
|
387
|
+
throw new Error(`terminal Watch root contains an unknown file: ${entryPath}`);
|
|
388
|
+
}
|
|
389
|
+
const watchId = entry.name.slice(0, -".json".length);
|
|
390
|
+
assertRecordId(watchId, "terminal Watch id");
|
|
391
|
+
if (!isolateInvalidRecord) {
|
|
392
|
+
watches.push(loadTerminalWatch(storeDir, watchId));
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// Establish the filesystem/security boundary before isolating JSON syntax
|
|
396
|
+
// or schema errors. A race, permission failure, or non-regular replacement
|
|
397
|
+
// must still abort the whole scan.
|
|
398
|
+
assertOwnerPrivateFile(entryPath, "terminal Watch state");
|
|
399
|
+
let value;
|
|
400
|
+
try {
|
|
401
|
+
value = readJsonFileNoFollow(entryPath, "terminal Watch state");
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (!(error instanceof SyntaxError))
|
|
405
|
+
throw error;
|
|
406
|
+
errors.push({
|
|
407
|
+
watch_id: watchId,
|
|
408
|
+
error_code: "terminal_watch_record_invalid"
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
watches.push(decodeTerminalWatch(value, watchId));
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
errors.push({
|
|
417
|
+
watch_id: watchId,
|
|
418
|
+
error_code: "terminal_watch_record_invalid"
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
watches.sort((left, right) => right.updated_at.localeCompare(left.updated_at) ||
|
|
423
|
+
left.watch_id.localeCompare(right.watch_id));
|
|
424
|
+
errors.sort((left, right) => left.watch_id.localeCompare(right.watch_id));
|
|
425
|
+
return { watches, errors };
|
|
426
|
+
}
|
|
427
|
+
function decodeTerminalWatch(value, expectedWatchId) {
|
|
428
|
+
const normalized = normalizeLegacyTerminalWatch(value);
|
|
429
|
+
assertTerminalWatch(normalized, expectedWatchId);
|
|
430
|
+
return normalized;
|
|
431
|
+
}
|
|
432
|
+
function normalizeLegacyTerminalWatch(value) {
|
|
433
|
+
if (!isRecord(value) ||
|
|
434
|
+
value.schema !== TERMINAL_WATCH_SCHEMA ||
|
|
435
|
+
value.version !== TERMINAL_WATCH_VERSION ||
|
|
436
|
+
value.observation_checkpoint !== undefined ||
|
|
437
|
+
!isRecord(value.anchor)) {
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
const anchor = value.anchor;
|
|
441
|
+
if (anchor.schema ===
|
|
442
|
+
"agent-knock-knock/claude-human-started-active-task-anchor") {
|
|
443
|
+
const validated = validateClaudeHumanStartedActiveTaskAnchor(anchor);
|
|
444
|
+
return {
|
|
445
|
+
...value,
|
|
446
|
+
observation_checkpoint: initialTerminalWatchObservationCheckpoint(validated)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (anchor.schema ===
|
|
450
|
+
"agent-knock-knock/codex-human-started-active-task-anchor") {
|
|
451
|
+
const validated = validateCodexHumanStartedActiveTaskAnchor(anchor);
|
|
452
|
+
return {
|
|
453
|
+
...value,
|
|
454
|
+
observation_checkpoint: initialTerminalWatchObservationCheckpoint(validated)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
function assertObservationCheckpoint(value, anchor) {
|
|
460
|
+
if (anchor.schema ===
|
|
461
|
+
"agent-knock-knock/claude-human-started-active-task-anchor") {
|
|
462
|
+
validateClaudeHumanStartedActiveTaskCheckpoint(value, anchor);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
assertStrictRecord(value, "terminal Watch observation checkpoint", {
|
|
466
|
+
safe_resume_offset_bytes: NON_NEGATIVE_INTEGER
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function assertTerminalIdentity(value) {
|
|
470
|
+
assertStrictRecord(value, "terminal Watch terminal identity", TERMINAL_IDENTITY_FIELDS);
|
|
471
|
+
}
|
|
472
|
+
function assertTerminalEndpoint(value) {
|
|
473
|
+
assertStrictRecord(value, "terminal Watch endpoint", TERMINAL_ENDPOINT_FIELDS);
|
|
474
|
+
if (!terminalEndpointIdentityFromEvidence(value) ||
|
|
475
|
+
!terminalRouteKeyFromEvidence(value)) {
|
|
476
|
+
throw new Error("terminal Watch endpoint evidence is not exact");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function assertTerminalWatchAnchor(value, agent, terminal) {
|
|
480
|
+
if (!isRecord(value)) {
|
|
481
|
+
throw new Error("terminal Watch anchor must be an object");
|
|
482
|
+
}
|
|
483
|
+
if (value.schema ===
|
|
484
|
+
"agent-knock-knock/codex-human-started-active-task-anchor") {
|
|
485
|
+
if (agent !== "codex") {
|
|
486
|
+
throw new Error("Codex Watch anchor cannot belong to another agent");
|
|
487
|
+
}
|
|
488
|
+
validateCodexHumanStartedActiveTaskAnchor(value);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (value.schema ===
|
|
492
|
+
"agent-knock-knock/claude-human-started-active-task-anchor") {
|
|
493
|
+
if (agent !== "claude") {
|
|
494
|
+
throw new Error("Claude Watch anchor cannot belong to another agent");
|
|
495
|
+
}
|
|
496
|
+
const anchor = validateClaudeHumanStartedActiveTaskAnchor(value);
|
|
497
|
+
if (anchor.cwd !== terminal.workspace) {
|
|
498
|
+
throw new Error("Claude Watch workspace does not match its task anchor");
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
throw new Error("terminal Watch anchor schema is unsupported");
|
|
503
|
+
}
|
|
504
|
+
function assertSettlement(value, status) {
|
|
505
|
+
if (status === "active") {
|
|
506
|
+
if (value !== undefined) {
|
|
507
|
+
throw new Error("an active terminal Watch cannot carry a settlement");
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
assertStrictRecord(value, "terminal Watch settlement", SETTLEMENT_FIELDS);
|
|
512
|
+
if (value.kind !== status) {
|
|
513
|
+
throw new Error("terminal Watch settlement kind must match its status");
|
|
514
|
+
}
|
|
515
|
+
if (!["completed", "failed"].includes(status) &&
|
|
516
|
+
(value.completion_text !== undefined ||
|
|
517
|
+
value.completion_id !== undefined ||
|
|
518
|
+
value.completion_timestamp !== undefined)) {
|
|
519
|
+
throw new Error("only completed or failed terminal Watches may carry completion output");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function assertNotificationOutbox(watch) {
|
|
523
|
+
const seenIds = new Set();
|
|
524
|
+
const seenEvidence = new Set();
|
|
525
|
+
let previousCreatedAt = Date.parse(watch.created_at);
|
|
526
|
+
for (const notification of watch.notification_outbox) {
|
|
527
|
+
assertNotification(notification, String(watch.watch_id));
|
|
528
|
+
if (seenIds.has(notification.notification_id)) {
|
|
529
|
+
throw new Error("terminal Watch notification ids must be unique");
|
|
530
|
+
}
|
|
531
|
+
const evidenceKey = `${notification.kind}:${notification.evidence_fingerprint}`;
|
|
532
|
+
if (seenEvidence.has(evidenceKey)) {
|
|
533
|
+
throw new Error("terminal Watch notification evidence must be unique");
|
|
534
|
+
}
|
|
535
|
+
const createdAt = Date.parse(notification.created_at);
|
|
536
|
+
if (createdAt < previousCreatedAt ||
|
|
537
|
+
createdAt > Date.parse(watch.updated_at)) {
|
|
538
|
+
throw new Error("terminal Watch notification timestamps are not monotonic");
|
|
539
|
+
}
|
|
540
|
+
previousCreatedAt = createdAt;
|
|
541
|
+
seenIds.add(notification.notification_id);
|
|
542
|
+
seenEvidence.add(evidenceKey);
|
|
543
|
+
}
|
|
544
|
+
if (watch.status !== "active") {
|
|
545
|
+
const settlement = watch.settlement;
|
|
546
|
+
const terminalNotifications = watch.notification_outbox.filter((notification) => notification.kind !== "approval");
|
|
547
|
+
if (terminalNotifications.length !== 1 ||
|
|
548
|
+
terminalNotifications[0].kind !== watch.status ||
|
|
549
|
+
terminalNotifications[0].evidence_fingerprint !==
|
|
550
|
+
settlement.evidence_fingerprint) {
|
|
551
|
+
throw new Error("terminal Watch settlement must have exactly one notification");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (watch.notification_outbox.some((notification) => notification.kind !== "approval")) {
|
|
555
|
+
throw new Error("an active terminal Watch cannot have an outcome notification");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function assertNotification(value, watchId) {
|
|
559
|
+
assertStrictRecord(value, "terminal Watch notification", NOTIFICATION_FIELDS);
|
|
560
|
+
const notification = value;
|
|
561
|
+
const expectedId = terminalWatchNotificationId(watchId, notification.kind, notification.evidence_fingerprint);
|
|
562
|
+
if (notification.notification_id !== expectedId ||
|
|
563
|
+
notification.idempotency_key !==
|
|
564
|
+
terminalWatchNotificationIdempotencyKey(watchId, expectedId)) {
|
|
565
|
+
throw new Error("terminal Watch notification identity is not deterministic");
|
|
566
|
+
}
|
|
567
|
+
const [minimumAttempts, receiptFields] = NOTIFICATION_SHAPES[notification.status];
|
|
568
|
+
assertNotificationShape(value, minimumAttempts, receiptFields);
|
|
569
|
+
}
|
|
570
|
+
function assertNotificationShape(value, minimumAttempts, required) {
|
|
571
|
+
if (Number(value.attempts) < minimumAttempts) {
|
|
572
|
+
throw new Error(`terminal Watch ${String(value.status)} notification has no attempt`);
|
|
573
|
+
}
|
|
574
|
+
for (const key of required) {
|
|
575
|
+
if (value[key] === undefined) {
|
|
576
|
+
throw new Error(`terminal Watch ${String(value.status)} notification requires ${key}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const allowed = new Set(required);
|
|
580
|
+
for (const key of Object.keys(NOTIFICATION_RECEIPT_FIELDS)) {
|
|
581
|
+
if (value[key] !== undefined && !allowed.has(key)) {
|
|
582
|
+
throw new Error(`terminal Watch ${String(value.status)} notification cannot carry ${key}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function assertTerminalWatchAdvance(current, candidate) {
|
|
587
|
+
for (const [label, before, after] of [
|
|
588
|
+
["schema", current.schema, candidate.schema],
|
|
589
|
+
["version", current.version, candidate.version],
|
|
590
|
+
["watch_id", current.watch_id, candidate.watch_id],
|
|
591
|
+
["agent", current.agent, candidate.agent],
|
|
592
|
+
["terminal", current.terminal, candidate.terminal],
|
|
593
|
+
["anchor", current.anchor, candidate.anchor],
|
|
594
|
+
["openclaw_session", current.openclaw_session, candidate.openclaw_session],
|
|
595
|
+
["openclaw_bin", current.openclaw_bin, candidate.openclaw_bin],
|
|
596
|
+
["created_at", current.created_at, candidate.created_at],
|
|
597
|
+
["deadline_at", current.deadline_at, candidate.deadline_at]
|
|
598
|
+
]) {
|
|
599
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
600
|
+
throw new Error(`terminal Watch cannot change immutable ${label}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (Date.parse(candidate.updated_at) < Date.parse(current.updated_at) ||
|
|
604
|
+
Date.parse(candidate.last_activity_at) < Date.parse(current.last_activity_at)) {
|
|
605
|
+
throw new Error("terminal Watch update cannot move time backwards");
|
|
606
|
+
}
|
|
607
|
+
if (candidate.observation_checkpoint.safe_resume_offset_bytes <
|
|
608
|
+
current.observation_checkpoint.safe_resume_offset_bytes) {
|
|
609
|
+
throw new Error("terminal Watch observation checkpoint cannot move backwards");
|
|
610
|
+
}
|
|
611
|
+
if (current.status !== "active" &&
|
|
612
|
+
candidate.status !== current.status) {
|
|
613
|
+
throw new Error("a settled terminal Watch cannot change outcome");
|
|
614
|
+
}
|
|
615
|
+
if (current.settlement !== undefined &&
|
|
616
|
+
JSON.stringify(candidate.settlement) !== JSON.stringify(current.settlement)) {
|
|
617
|
+
throw new Error("terminal Watch settlement is immutable");
|
|
618
|
+
}
|
|
619
|
+
if (current.status !== "active") {
|
|
620
|
+
if (candidate.last_activity_at !== current.last_activity_at ||
|
|
621
|
+
JSON.stringify(candidate.observation_checkpoint) !==
|
|
622
|
+
JSON.stringify(current.observation_checkpoint) ||
|
|
623
|
+
candidate.notification_outbox.length !== current.notification_outbox.length) {
|
|
624
|
+
throw new Error("a settled terminal Watch may change only notification delivery receipts");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
else if (candidate.notification_outbox.length >
|
|
628
|
+
current.notification_outbox.length + 1) {
|
|
629
|
+
throw new Error("terminal Watch may append at most one notification per update");
|
|
630
|
+
}
|
|
631
|
+
assertNotificationAdvance(current.notification_outbox, candidate.notification_outbox);
|
|
632
|
+
}
|
|
633
|
+
function assertNotificationAdvance(current, candidate) {
|
|
634
|
+
if (candidate.length < current.length) {
|
|
635
|
+
throw new Error("terminal Watch notification outbox is append-only");
|
|
636
|
+
}
|
|
637
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
638
|
+
const before = current[index];
|
|
639
|
+
const after = candidate[index];
|
|
640
|
+
for (const key of [
|
|
641
|
+
"notification_id",
|
|
642
|
+
"idempotency_key",
|
|
643
|
+
"kind",
|
|
644
|
+
"evidence_fingerprint",
|
|
645
|
+
"reason_code",
|
|
646
|
+
"created_at"
|
|
647
|
+
]) {
|
|
648
|
+
if (before[key] !== after[key]) {
|
|
649
|
+
throw new Error(`terminal Watch notification cannot change immutable ${key}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
assertNotificationStatusAdvance(before, after);
|
|
653
|
+
}
|
|
654
|
+
for (const added of candidate.slice(current.length)) {
|
|
655
|
+
if (added.status !== "pending" || added.attempts !== 0) {
|
|
656
|
+
throw new Error("a terminal Watch notification must be appended pending");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function assertNotificationStatusAdvance(current, candidate) {
|
|
661
|
+
const allowed = {
|
|
662
|
+
pending: ["pending", "delivering", "superseded"],
|
|
663
|
+
delivering: [
|
|
664
|
+
"delivering", "failed", "delivered", "superseded"
|
|
665
|
+
],
|
|
666
|
+
failed: ["failed", "delivering", "superseded"],
|
|
667
|
+
delivered: ["delivered"],
|
|
668
|
+
superseded: ["superseded"]
|
|
669
|
+
};
|
|
670
|
+
if (!allowed[current.status].includes(candidate.status)) {
|
|
671
|
+
throw new Error(`terminal Watch notification cannot advance ${current.status} to ${candidate.status}`);
|
|
672
|
+
}
|
|
673
|
+
const reclaimed = current.status === "delivering" &&
|
|
674
|
+
candidate.status === "delivering" &&
|
|
675
|
+
candidate.attempt_id !== current.attempt_id;
|
|
676
|
+
const claimed = ["pending", "failed"].includes(current.status) &&
|
|
677
|
+
candidate.status === "delivering";
|
|
678
|
+
const expectedAttempts = claimed || reclaimed
|
|
679
|
+
? current.attempts + 1
|
|
680
|
+
: current.attempts;
|
|
681
|
+
if (candidate.attempts !== expectedAttempts) {
|
|
682
|
+
throw new Error("terminal Watch notification attempt count is not monotonic");
|
|
683
|
+
}
|
|
684
|
+
if (current.status === candidate.status &&
|
|
685
|
+
!reclaimed &&
|
|
686
|
+
JSON.stringify(current) !== JSON.stringify(candidate)) {
|
|
687
|
+
throw new Error("terminal Watch notification receipt cannot change without a phase advance");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function atomicSaveTerminalWatch(filePath, watch) {
|
|
691
|
+
atomicSaveJsonFile(filePath, watch, {
|
|
692
|
+
rootLabel: "AKK Store",
|
|
693
|
+
directoryLabel: "terminal Watch root",
|
|
694
|
+
fileLabel: "terminal Watch state",
|
|
695
|
+
ensureDirectory: ensureDir,
|
|
696
|
+
fsyncNewDirectoryParent: true
|
|
697
|
+
});
|
|
698
|
+
assertOwnerPrivateDirectory(path.dirname(filePath), "terminal Watch root");
|
|
699
|
+
assertOwnerPrivateFile(filePath, "terminal Watch state");
|
|
700
|
+
}
|
|
701
|
+
function isRecognizedWatchLockFile(name) {
|
|
702
|
+
const suffix = name.endsWith(".json.lock.reclaim")
|
|
703
|
+
? ".json.lock.reclaim"
|
|
704
|
+
: name.endsWith(".json.lock")
|
|
705
|
+
? ".json.lock"
|
|
706
|
+
: undefined;
|
|
707
|
+
if (!suffix) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
const watchId = name.slice(0, -suffix.length);
|
|
711
|
+
try {
|
|
712
|
+
assertRecordId(watchId, "terminal Watch lock id");
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function isRecognizedWatchTemporaryFile(name) {
|
|
720
|
+
const match = /^\.(.+)\.json\.([1-9]\d*)\.([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u.exec(name);
|
|
721
|
+
if (!match)
|
|
722
|
+
return false;
|
|
723
|
+
try {
|
|
724
|
+
assertRecordId(match[1], "terminal Watch temporary id");
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function assertPrivateTransientEntry(filePath) {
|
|
732
|
+
try {
|
|
733
|
+
assertOwnerPrivateFile(filePath, "terminal Watch transient file");
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
if (!isNodeError(error, "ENOENT"))
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function assertOwnerPrivateDirectory(directory, label) {
|
|
741
|
+
assertRealDirectory(directory, label);
|
|
742
|
+
const mode = fs.lstatSync(directory).mode & 0o777;
|
|
743
|
+
if (mode !== PRIVATE_DIRECTORY_MODE) {
|
|
744
|
+
throw new Error(`${label} must have owner-private 0700 permissions: ${directory}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function assertOwnerPrivateFile(filePath, label) {
|
|
748
|
+
const stat = fs.lstatSync(filePath);
|
|
749
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
750
|
+
throw new Error(`${label} must be a regular file: ${filePath}`);
|
|
751
|
+
}
|
|
752
|
+
if ((stat.mode & 0o777) !== PRIVATE_FILE_MODE) {
|
|
753
|
+
throw new Error(`${label} must have owner-private 0600 permissions: ${filePath}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function assertStrictRecord(value, label, shape) {
|
|
757
|
+
if (!isRecord(value))
|
|
758
|
+
throw new Error(`${label} must be an object`);
|
|
759
|
+
const unknown = Object.keys(value).find((key) => !Object.hasOwn(shape, key));
|
|
760
|
+
if (unknown) {
|
|
761
|
+
throw new Error(`${label} contains unsupported field ${unknown}`);
|
|
762
|
+
}
|
|
763
|
+
for (const [key, guard] of Object.entries(shape)) {
|
|
764
|
+
guard(value[key], `${label} ${key}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function literalGuard(expected) {
|
|
768
|
+
return (value, label) => {
|
|
769
|
+
if (value !== expected)
|
|
770
|
+
throw new Error(`${label} is invalid`);
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function oneOfGuard(allowed) {
|
|
774
|
+
return (value, label) => {
|
|
775
|
+
if (!allowed.includes(value))
|
|
776
|
+
throw new Error(`${label} is invalid`);
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function optionalGuard(guard) {
|
|
780
|
+
return (value, label) => {
|
|
781
|
+
if (value !== undefined)
|
|
782
|
+
guard(value, label);
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function nullableGuard(guard) {
|
|
786
|
+
return (value, label) => {
|
|
787
|
+
if (value !== null)
|
|
788
|
+
guard(value, label);
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function assertExpectedRevision(value) {
|
|
792
|
+
if (value !== null && !isPositiveSafeInteger(value)) {
|
|
793
|
+
throw new Error("expectedRevision must be null or a positive safe integer");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function assertRecordId(value, label) {
|
|
797
|
+
if (typeof value !== "string" ||
|
|
798
|
+
value.trim().length === 0 ||
|
|
799
|
+
value.includes("\0") ||
|
|
800
|
+
value === "." ||
|
|
801
|
+
value === ".." ||
|
|
802
|
+
path.posix.basename(value) !== value ||
|
|
803
|
+
path.win32.basename(value) !== value ||
|
|
804
|
+
path.isAbsolute(value)) {
|
|
805
|
+
throw new Error(`${label} is not safe for storage: ${String(value)}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function assertContained(candidate, parent, label) {
|
|
809
|
+
const resolvedParent = path.resolve(parent);
|
|
810
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
811
|
+
if (resolvedCandidate === resolvedParent ||
|
|
812
|
+
!resolvedCandidate.startsWith(`${resolvedParent}${path.sep}`)) {
|
|
813
|
+
throw new Error(`${label} escapes its Store root: ${candidate}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function assertNotificationKind(value) {
|
|
817
|
+
if (!TERMINAL_WATCH_NOTIFICATION_KINDS.includes(value)) {
|
|
818
|
+
throw new Error("terminal Watch notification kind is invalid");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function isPositiveSafeInteger(value) {
|
|
822
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
823
|
+
}
|
|
824
|
+
function isNonNegativeSafeInteger(value) {
|
|
825
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
826
|
+
}
|
|
827
|
+
function assertNonEmptyString(value, label) {
|
|
828
|
+
if (typeof value !== "string" ||
|
|
829
|
+
value.trim().length === 0 ||
|
|
830
|
+
value.includes("\0")) {
|
|
831
|
+
throw new Error(`${label} must be a non-empty string without NUL bytes`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function assertSha256(value, label) {
|
|
835
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) {
|
|
836
|
+
throw new Error(`${label} must be a lowercase SHA-256 digest`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function assertTimestamp(value, label) {
|
|
840
|
+
if (typeof value !== "string" ||
|
|
841
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
842
|
+
new Date(value).toISOString() !== value) {
|
|
843
|
+
throw new Error(`${label} must be a canonical ISO timestamp`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function assertReasonCode(value, label) {
|
|
847
|
+
if (typeof value !== "string" ||
|
|
848
|
+
value.length < 1 ||
|
|
849
|
+
value.length > 128 ||
|
|
850
|
+
!/^[a-z0-9][a-z0-9_.:-]*$/u.test(value)) {
|
|
851
|
+
throw new Error(`${label} must be a privacy-safe reason code`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function assertCompletionText(value, label) {
|
|
855
|
+
if (typeof value !== "string" ||
|
|
856
|
+
value.length > 4000 ||
|
|
857
|
+
value.includes("\0")) {
|
|
858
|
+
throw new Error(`${label} must be redacted and at most 4k`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
//# sourceMappingURL=terminal-watch-store.js.map
|