@naswerks/periscope 1.0.0

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -0
  4. package/SECURITY.md +315 -0
  5. package/dist/bin/command.d.ts +97 -0
  6. package/dist/bin/command.js +141 -0
  7. package/dist/bin/config.d.ts +7 -0
  8. package/dist/bin/config.js +90 -0
  9. package/dist/bin/login.d.ts +48 -0
  10. package/dist/bin/login.js +82 -0
  11. package/dist/bin/main.d.ts +33 -0
  12. package/dist/bin/main.js +173 -0
  13. package/dist/bin/pair.d.ts +28 -0
  14. package/dist/bin/pair.js +198 -0
  15. package/dist/bin/periscope.d.ts +3 -0
  16. package/dist/bin/periscope.js +4 -0
  17. package/dist/bin/posture.d.ts +64 -0
  18. package/dist/bin/posture.js +157 -0
  19. package/dist/bin/reconfigure.d.ts +64 -0
  20. package/dist/bin/reconfigure.js +195 -0
  21. package/dist/bin/serve.d.ts +103 -0
  22. package/dist/bin/serve.js +441 -0
  23. package/dist/bin/status.d.ts +11 -0
  24. package/dist/bin/status.js +49 -0
  25. package/dist/bin/workspaces.d.ts +109 -0
  26. package/dist/bin/workspaces.js +215 -0
  27. package/dist/control/backoff.d.ts +25 -0
  28. package/dist/control/backoff.js +20 -0
  29. package/dist/control/codec.d.ts +15 -0
  30. package/dist/control/codec.js +501 -0
  31. package/dist/control/credential.d.ts +23 -0
  32. package/dist/control/credential.js +8 -0
  33. package/dist/control/frames.d.ts +1180 -0
  34. package/dist/control/frames.js +441 -0
  35. package/dist/control/link-state.d.ts +32 -0
  36. package/dist/control/link-state.js +39 -0
  37. package/dist/control/link.d.ts +109 -0
  38. package/dist/control/link.js +736 -0
  39. package/dist/control/queue.d.ts +110 -0
  40. package/dist/control/queue.js +174 -0
  41. package/dist/control/seq.d.ts +49 -0
  42. package/dist/control/seq.js +52 -0
  43. package/dist/control/stream-routing.d.ts +200 -0
  44. package/dist/control/stream-routing.js +167 -0
  45. package/dist/control/stream.d.ts +43 -0
  46. package/dist/control/stream.js +64 -0
  47. package/dist/core/async-queue.d.ts +26 -0
  48. package/dist/core/async-queue.js +63 -0
  49. package/dist/core/failure.d.ts +22 -0
  50. package/dist/core/failure.js +53 -0
  51. package/dist/core/index.d.ts +18 -0
  52. package/dist/core/index.js +7 -0
  53. package/dist/core/keyed-turns.d.ts +28 -0
  54. package/dist/core/keyed-turns.js +44 -0
  55. package/dist/core/paths.d.ts +25 -0
  56. package/dist/core/paths.js +77 -0
  57. package/dist/core/refusal.d.ts +43 -0
  58. package/dist/core/refusal.js +399 -0
  59. package/dist/core/result.d.ts +22 -0
  60. package/dist/core/result.js +16 -0
  61. package/dist/core/time.d.ts +19 -0
  62. package/dist/core/time.js +25 -0
  63. package/dist/core/vocab.d.ts +23 -0
  64. package/dist/core/vocab.js +37 -0
  65. package/dist/core/workspace-id.d.ts +12 -0
  66. package/dist/core/workspace-id.js +12 -0
  67. package/dist/gate/command.d.ts +136 -0
  68. package/dist/gate/command.js +551 -0
  69. package/dist/gate/decision.d.ts +111 -0
  70. package/dist/gate/decision.js +97 -0
  71. package/dist/gate/escalate.d.ts +82 -0
  72. package/dist/gate/escalate.js +92 -0
  73. package/dist/gate/gate.d.ts +173 -0
  74. package/dist/gate/gate.js +417 -0
  75. package/dist/gate/index.d.ts +39 -0
  76. package/dist/gate/index.js +31 -0
  77. package/dist/gate/jail.d.ts +92 -0
  78. package/dist/gate/jail.js +131 -0
  79. package/dist/gate/local.d.ts +69 -0
  80. package/dist/gate/local.js +51 -0
  81. package/dist/gate/outcome.d.ts +72 -0
  82. package/dist/gate/outcome.js +91 -0
  83. package/dist/gate/shell.d.ts +36 -0
  84. package/dist/gate/shell.js +226 -0
  85. package/dist/host/agent-process.d.ts +452 -0
  86. package/dist/host/agent-process.js +382 -0
  87. package/dist/host/bulk-post.d.ts +44 -0
  88. package/dist/host/bulk-post.js +114 -0
  89. package/dist/host/claude-transcripts.d.ts +84 -0
  90. package/dist/host/claude-transcripts.js +339 -0
  91. package/dist/host/config-file.d.ts +60 -0
  92. package/dist/host/config-file.js +216 -0
  93. package/dist/host/git-facts.d.ts +7 -0
  94. package/dist/host/git-facts.js +106 -0
  95. package/dist/host/hooks.d.ts +56 -0
  96. package/dist/host/hooks.js +75 -0
  97. package/dist/host/host.d.ts +356 -0
  98. package/dist/host/host.js +1294 -0
  99. package/dist/host/index.d.ts +35 -0
  100. package/dist/host/index.js +22 -0
  101. package/dist/host/link-state-file.d.ts +18 -0
  102. package/dist/host/link-state-file.js +66 -0
  103. package/dist/host/loopback.d.ts +38 -0
  104. package/dist/host/loopback.js +122 -0
  105. package/dist/host/machine.d.ts +9 -0
  106. package/dist/host/machine.js +19 -0
  107. package/dist/host/mcp-server.d.ts +11 -0
  108. package/dist/host/mcp-server.js +48 -0
  109. package/dist/host/package-facts.d.ts +3 -0
  110. package/dist/host/package-facts.js +26 -0
  111. package/dist/host/paired-credential-store.d.ts +32 -0
  112. package/dist/host/paired-credential-store.js +112 -0
  113. package/dist/host/paths.d.ts +80 -0
  114. package/dist/host/paths.js +165 -0
  115. package/dist/host/repository-read.d.ts +33 -0
  116. package/dist/host/repository-read.js +201 -0
  117. package/dist/host/session-store.d.ts +53 -0
  118. package/dist/host/session-store.js +100 -0
  119. package/dist/host/sign-in.d.ts +90 -0
  120. package/dist/host/sign-in.js +239 -0
  121. package/dist/host/telemetry.d.ts +42 -0
  122. package/dist/host/telemetry.js +74 -0
  123. package/dist/host/token-cache.d.ts +62 -0
  124. package/dist/host/token-cache.js +185 -0
  125. package/dist/host/transcript-fs.d.ts +4 -0
  126. package/dist/host/transcript-fs.js +104 -0
  127. package/dist/host/wire-request.d.ts +80 -0
  128. package/dist/host/wire-request.js +196 -0
  129. package/dist/host/workspace-fs.d.ts +14 -0
  130. package/dist/host/workspace-fs.js +84 -0
  131. package/dist/host/workspace-trust.d.ts +23 -0
  132. package/dist/host/workspace-trust.js +80 -0
  133. package/dist/identity/authorize.d.ts +59 -0
  134. package/dist/identity/authorize.js +72 -0
  135. package/dist/identity/config.d.ts +98 -0
  136. package/dist/identity/config.js +130 -0
  137. package/dist/identity/credential.d.ts +91 -0
  138. package/dist/identity/credential.js +76 -0
  139. package/dist/identity/device-code.d.ts +115 -0
  140. package/dist/identity/device-code.js +134 -0
  141. package/dist/identity/index.d.ts +33 -0
  142. package/dist/identity/index.js +10 -0
  143. package/dist/identity/mode.d.ts +106 -0
  144. package/dist/identity/mode.js +69 -0
  145. package/dist/identity/paired-credential.d.ts +52 -0
  146. package/dist/identity/paired-credential.js +43 -0
  147. package/dist/identity/pkce.d.ts +50 -0
  148. package/dist/identity/pkce.js +94 -0
  149. package/dist/identity/store.d.ts +43 -0
  150. package/dist/identity/store.js +43 -0
  151. package/dist/identity/token.d.ts +58 -0
  152. package/dist/identity/token.js +149 -0
  153. package/dist/index.d.ts +45 -0
  154. package/dist/index.js +53 -0
  155. package/dist/mcp/descriptor.d.ts +59 -0
  156. package/dist/mcp/descriptor.js +14 -0
  157. package/dist/mcp/index.d.ts +14 -0
  158. package/dist/mcp/index.js +4 -0
  159. package/dist/mcp/schema.d.ts +37 -0
  160. package/dist/mcp/schema.js +175 -0
  161. package/dist/mcp/server.d.ts +107 -0
  162. package/dist/mcp/server.js +66 -0
  163. package/dist/persistence/entry.d.ts +74 -0
  164. package/dist/persistence/entry.js +105 -0
  165. package/dist/persistence/index.d.ts +22 -0
  166. package/dist/persistence/index.js +8 -0
  167. package/dist/persistence/key.d.ts +46 -0
  168. package/dist/persistence/key.js +33 -0
  169. package/dist/persistence/mirror.d.ts +71 -0
  170. package/dist/persistence/mirror.js +57 -0
  171. package/dist/persistence/receipt.d.ts +111 -0
  172. package/dist/persistence/receipt.js +85 -0
  173. package/dist/persistence/retention.d.ts +68 -0
  174. package/dist/persistence/retention.js +68 -0
  175. package/dist/persistence/store.d.ts +106 -0
  176. package/dist/persistence/store.js +86 -0
  177. package/dist/persistence/transition-log.d.ts +73 -0
  178. package/dist/persistence/transition-log.js +133 -0
  179. package/dist/protocol.d.ts +27 -0
  180. package/dist/protocol.js +12 -0
  181. package/dist/sessions/index.d.ts +8 -0
  182. package/dist/sessions/index.js +4 -0
  183. package/dist/sessions/registry.d.ts +186 -0
  184. package/dist/sessions/registry.js +190 -0
  185. package/dist/sessions/session.d.ts +178 -0
  186. package/dist/sessions/session.js +288 -0
  187. package/dist/sessions/spawn-env.d.ts +88 -0
  188. package/dist/sessions/spawn-env.js +277 -0
  189. package/dist/state/coverage.d.ts +308 -0
  190. package/dist/state/coverage.js +315 -0
  191. package/dist/state/index.d.ts +12 -0
  192. package/dist/state/index.js +7 -0
  193. package/dist/state/machine.d.ts +112 -0
  194. package/dist/state/machine.js +237 -0
  195. package/dist/state/model.d.ts +233 -0
  196. package/dist/state/model.js +214 -0
  197. package/dist/state/observer.d.ts +55 -0
  198. package/dist/state/observer.js +413 -0
  199. package/dist/state/reporter.d.ts +45 -0
  200. package/dist/state/reporter.js +35 -0
  201. package/dist/state/store.d.ts +56 -0
  202. package/dist/state/store.js +120 -0
  203. package/dist/telemetry/index.d.ts +11 -0
  204. package/dist/telemetry/index.js +2 -0
  205. package/dist/telemetry/usage.d.ts +78 -0
  206. package/dist/telemetry/usage.js +69 -0
  207. package/dist/workspace/git-worktree.d.ts +150 -0
  208. package/dist/workspace/git-worktree.js +417 -0
  209. package/dist/workspace/index.d.ts +9 -0
  210. package/dist/workspace/index.js +4 -0
  211. package/dist/workspace/plain-dir.d.ts +34 -0
  212. package/dist/workspace/plain-dir.js +90 -0
  213. package/dist/workspace/provider.d.ts +152 -0
  214. package/dist/workspace/provider.js +2 -0
  215. package/dist/workspace/worktree-porcelain.d.ts +29 -0
  216. package/dist/workspace/worktree-porcelain.js +100 -0
  217. package/docs/architecture.md +277 -0
  218. package/docs/configuration.md +187 -0
  219. package/docs/gate.md +219 -0
  220. package/docs/identity.md +107 -0
  221. package/docs/protocol.md +501 -0
  222. package/docs/state-machine.md +160 -0
  223. package/examples/README.md +141 -0
  224. package/examples/minimal-controller/controller.ts +38 -0
  225. package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
  226. package/examples/parallel-run-proof/run.ts +850 -0
  227. package/examples/test-controller/controller.ts +655 -0
  228. package/examples/test-controller/serve.ts +29 -0
  229. package/examples/tsconfig.json +30 -0
  230. package/package.json +98 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The closed vocabulary of reasons this package refuses something.
3
+ *
4
+ * A degrade is a named outcome. Nothing here returns a bare `false`, an empty string, or a silent
5
+ * skip: a caller that cannot proceed learns which of these it hit, and the set is closed so a
6
+ * reader can enumerate every way a call can fail without reading every call site.
7
+ */
8
+ export declare const REFUSAL_REASONS: readonly ["frame-not-json", "frame-malformed", "frame-too-large", "seq-gap", "seq-regressed", "link-not-open", "link-closed", "link-send-failed", "link-unauthorized", "link-write-deferred", "queue-dropped-droppable", "queue-overflow-undroppable", "retention-released-unacked", "credential-unavailable", "session-unknown", "session-spawn-failed", "workspace-untrusted", "transition-cause-unnamed", "permission-decision-unavailable", "permission-decision-unrecognised", "permission-grant-shadows-settings", "gate-deadlines-inverted", "bulk-target-invalid", "bulk-target-not-controller", "bulk-delivery-failed", "path-not-absolute", "path-escapes-root", "path-input-missing", "path-unresolvable", "credential-path-denied", "shell-command-missing", "shell-boundary-command", "shell-verb-unrecognised", "identity-not-configured", "identity-config-invalid", "token-unavailable", "auth-state-mismatch", "auth-callback-refused", "pkce-method-unsupported", "token-request-failed", "token-response-invalid", "token-grant-rejected", "credential-cache-unreadable", "credential-cache-write-failed", "credential-mode-too-wide", "credential-mode-unenforced", "credential-mode-unobservable", "device-code-not-enabled", "device-code-declined", "workspace-provision-failed", "workspace-release-failed", "resume-cwd-not-honoured", "mcp-descriptor-invalid", "mcp-schema-unsupported", "mcp-tool-input-invalid", "transcript-key-invalid", "transcript-entry-malformed", "transcript-read-failed", "transcript-write-failed", "receipt-anchor-unknown", "retention-window-invalid", "gate-cannot-grant", "transcript-path-escape", "config-key-unknown", "config-value-invalid", "config-host-busy", "config-write-failed", "workspace-list-failed", "repository-path-escape", "repository-read-failed", "branch-not-merged"];
9
+ export type RefusalReason = (typeof REFUSAL_REASONS)[number];
10
+ /** A named refusal. `detail` is for a human reading a log; `reason` is what code branches on. */
11
+ export interface Refusal {
12
+ readonly reason: RefusalReason;
13
+ readonly detail: string;
14
+ }
15
+ export declare function refusal(reason: RefusalReason, detail: string): Refusal;
16
+ /**
17
+ * The reasons a retry cannot help with: the credential is gone until a person restores it.
18
+ *
19
+ * This is a predicate and not a comment because the link's reconnect loop is correct for every
20
+ * transient failure and actively harmful for these: it dials a door that structurally cannot open,
21
+ * forever, while the one fact the operator could act on scrolls past as another retry. So "is this
22
+ * terminal" has to be a thing code branches on, and it has to have one home; a second list
23
+ * somewhere else is how the link and the reporter end up disagreeing about whether a host is dead.
24
+ *
25
+ * The set is deliberately small, and erring toward transient is the safe direction. Treating a
26
+ * recoverable failure as terminal takes down a host that would have healed; treating a terminal one
27
+ * as recoverable costs a retry loop that is loud. Only reasons whose own detail already ends in
28
+ * "sign in again" belong here.
29
+ *
30
+ * `link-unauthorized` is the third member, and it is the only one the token layer cannot raise.
31
+ * The provider judged the material and approved it; the controller refused it at the upgrade. The
32
+ * conclusion is identical (the next dial with the same identity refuses identically, and only a
33
+ * person can change what is presented), so it takes the same exit rather than a second, private
34
+ * notion of "terminal" living in the link.
35
+ *
36
+ * `credential-unavailable` is not one of them, and must never be. It is what a host with no
37
+ * identity configured reports on every dial, and that host is supposed to keep connecting without a
38
+ * header, the supported no-identity mode. Putting it here would turn the most common unconfigured
39
+ * setup into a host that refuses to start.
40
+ */
41
+ export declare function needsHumanReauthentication(reason: RefusalReason): boolean;
42
+ export declare function isRefusalReason(value: string): value is RefusalReason;
43
+ //# sourceMappingURL=refusal.d.ts.map
@@ -0,0 +1,399 @@
1
+ /**
2
+ * The closed vocabulary of reasons this package refuses something.
3
+ *
4
+ * A degrade is a named outcome. Nothing here returns a bare `false`, an empty string, or a silent
5
+ * skip: a caller that cannot proceed learns which of these it hit, and the set is closed so a
6
+ * reader can enumerate every way a call can fail without reading every call site.
7
+ */
8
+ export const REFUSAL_REASONS = [
9
+ // Codec — the wire edge.
10
+ 'frame-not-json',
11
+ 'frame-malformed',
12
+ 'frame-too-large',
13
+ // Sequencing.
14
+ 'seq-gap',
15
+ 'seq-regressed',
16
+ // The outbound link.
17
+ // link-write-deferred The controller's own send outcome: the frame was validated,
18
+ // minted its seq and is retained, but the socket write did not land
19
+ // (closed, or faulted mid-write); the reconnect replay delivers it. It is
20
+ // not `link-send-failed`: that means never-queued, nothing coming. The
21
+ // split exists so that neither is reported as success on the controller
22
+ // (bytes that went nowhere, a caller told "sent"). Never emitted by this
23
+ // host; declared here because the vocabulary is shared.
24
+ // link-unauthorized The controller refused this host's identity at the door: the
25
+ // WebSocket upgrade answered 401 or 403, so no socket ever opened. It is
26
+ // not `link-send-failed`: that names a transport that faltered, and this
27
+ // transport worked perfectly while the peer said no. The token layer's own
28
+ // split (`token-grant-rejected`) cannot see this one, because the material
29
+ // was minted fine and refused later, at the controller; a revoked or
30
+ // mis-scoped credential presents exactly here. Terminal by predicate
31
+ // (see `needsHumanReauthentication`): redialling a door that has refused
32
+ // the identity is a loop with no exit. Sign in again.
33
+ 'link-not-open',
34
+ 'link-closed',
35
+ 'link-send-failed',
36
+ 'link-unauthorized',
37
+ 'link-write-deferred',
38
+ // The offline queue.
39
+ // retention-released-unacked An ended session's written frames, released because the hold
40
+ // outlived its bound, never because they were acked. It is not
41
+ // `queue-dropped-droppable`: that names a delta discarded before it
42
+ // was numbered, which the wire never misses. These are stamped,
43
+ // undroppable frames a controller could still have asked for, so
44
+ // losing them is a real loss and it says so. Named rather than
45
+ // silent, because "the host released them" and "they were never
46
+ // sent" are indistinguishable from the outside and only one of them
47
+ // is something to investigate.
48
+ 'queue-dropped-droppable',
49
+ 'queue-overflow-undroppable',
50
+ 'retention-released-unacked',
51
+ // No credential configured. The link connects without a header in that case (see `link.ts`).
52
+ 'credential-unavailable',
53
+ // Sessions. Three reasons, and they must not be collapsed into one: they are three different
54
+ // kinds of failure and only the caller can decide what to do about each.
55
+ // session-unknown a caller error: an id this registry does not hold. Retrying is futile.
56
+ // session-spawn-failed an outage: the agent process did not come up. Retrying may work.
57
+ // workspace-untrusted a degrade: the work proceeds, but settings-file rules are silently
58
+ // void, so anything depending on them is not in force.
59
+ // Merging them would let an outage arrive wearing a refusal's clothes and impersonate a
60
+ // deliberate "no".
61
+ 'session-unknown',
62
+ 'session-spawn-failed',
63
+ 'workspace-untrusted',
64
+ // The declared state model. A transition that cannot name what caused it is refused rather than
65
+ // recorded, because a state nobody can explain is worse than no state at all: it reads as fact.
66
+ 'transition-cause-unnamed',
67
+ // The permission decision. Two reasons, and they must not be collapsed into one; neither is a
68
+ // denial, which is a deliberate answer and carries `cause.kind:'control'` instead.
69
+ // permission-decision-unavailable an outage: the decider threw, the escalation transport
70
+ // failed, or the controller answered non-2xx. Nobody decided.
71
+ // permission-decision-unrecognised a decision value this build has never seen: a controller
72
+ // running ahead of a host, a new tier, a rolled-back deploy.
73
+ // An outage is an infrastructure fix and an unrecognised value is a controller-version fix, so
74
+ // telling them apart is the difference between two different investigations. Both block the tool.
75
+ // permission-grant-shadows-settings A composition this package will not assemble, refused
76
+ // before any session exists. The gate can make its allow
77
+ // effective (see `grantOnAllow` in gate/gate.ts), but an
78
+ // effective allow short-circuits every permission check that
79
+ // runs after it, and when settings files are loaded those
80
+ // checks include the operator's own deny rules. The two are
81
+ // individually reasonable and jointly mean "the gate silently
82
+ // overrides rules the operator wrote". Refused by name rather
83
+ // than resolved by precedence, because either precedence is a
84
+ // surprise to somebody.
85
+ // gate-deadlines-inverted The second composition this package will not assemble,
86
+ // reachable from off-box because gate timings ride
87
+ // `session_new`. `permissionHooks` throws at construction
88
+ // when the host's own deadline does not expire before the
89
+ // matcher's (correct, deliberate, and pinned, because a
90
+ // matcher expiring first blocks the tool with nothing
91
+ // recorded and reads as a hang). But a throw escaping the
92
+ // payload dispatcher is a command that vanishes, which reads
93
+ // to a controller as a host that hung: the exact failure the
94
+ // dispatcher refuses everything by name to avoid. So the
95
+ // throw stays where an embedder meets it and the composer
96
+ // converts it into this named refusal, before any process
97
+ // exists.
98
+ 'permission-decision-unavailable',
99
+ 'permission-decision-unrecognised',
100
+ 'permission-grant-shadows-settings',
101
+ 'gate-deadlines-inverted',
102
+ // The bulk lane.
103
+ //
104
+ // `bulk-target-not-controller` is a separate reason from `bulk-target-invalid`, deliberately.
105
+ // A malformed or wrong-scheme URL is a peer that sent nonsense; an origin that parses perfectly
106
+ // and simply is not this host's controller is a peer trying to make the host POST a transcript
107
+ // and its durable credential somewhere else. The second is the only one of the two a reader
108
+ // should be able to grep for, alert on, or count; folding it into the first would make an
109
+ // exfiltration attempt indistinguishable from a typo in a URL.
110
+ 'bulk-target-invalid',
111
+ 'bulk-target-not-controller',
112
+ 'bulk-delivery-failed',
113
+ // Paths.
114
+ 'path-not-absolute',
115
+ 'path-escapes-root',
116
+ // The host's own gate: refusals this package decides by itself, with no controller involved.
117
+ //
118
+ // They are refusal reasons rather than deny messages, and that is the contract.
119
+ // A deny decided by a controller is recorded `control/permission_denied`. If a locally-decided
120
+ // refusal were also expressed as a deny it would carry that same cause, and the only thing telling
121
+ // a local policy answer apart from a remote one would be free text in `detail` — which is
122
+ // documented as never branched on. Naming them here makes the difference a fact code can read:
123
+ // `state/model.ts`'s `CauseEvent` already admits every RefusalReason, so each of these becomes
124
+ // `refusal/<name>` on the transition, distinct both from a controller's deny and from
125
+ // `permission-decision-unavailable`, which means nobody decided at all.
126
+ //
127
+ // path-input-missing a path-taking tool whose input carries no readable path. A call
128
+ // whose target cannot be found is not one that can be bounded.
129
+ // path-unresolvable normalizing the path failed. Unresolvable is not provably inside.
130
+ // credential-path-denied the call targets the host's own credential material. The agent runs
131
+ // as the same OS user as the host, so file permissions are not a
132
+ // boundary against it and this is the only control that is.
133
+ // shell-command-missing a shell tool whose input carries no readable command.
134
+ // shell-boundary-command a named boundary operation: publishing, remote surgery, branch
135
+ // deletion, merging. Reported with the rule that fired.
136
+ // shell-verb-unrecognised a git invocation whose verb is not provably safe. Not the same as
137
+ // the above: this one fires on a verb nobody enumerated, the layer
138
+ // that exists because a denylist under-includes (the plumbing behind
139
+ // a publish can match none of its patterns).
140
+ 'path-input-missing',
141
+ 'path-unresolvable',
142
+ 'credential-path-denied',
143
+ 'shell-command-missing',
144
+ 'shell-boundary-command',
145
+ 'shell-verb-unrecognised',
146
+ // Identity: acquiring, holding and presenting the user's own token.
147
+ //
148
+ // The split between `identity-not-configured` and `token-unavailable` is the one that matters.
149
+ // The first says the operator has not set this host up; the second says they have, and nobody has
150
+ // signed in yet (or the token aged out and there is no refresh token). They are different people's
151
+ // problems on different days, and collapsing them would make the host's most common message
152
+ // useless: "not configured" sent to someone who configured it a week ago reads as a bug.
153
+ //
154
+ // identity-not-configured no authority/client id is set. The host cannot even try.
155
+ // identity-config-invalid set, but unusable: a non-https authority, a malformed redirect.
156
+ // token-unavailable configured and valid, but there is no usable token right now.
157
+ //
158
+ // `auth-state-mismatch` is a security event, not a protocol hiccup, and it is named separately
159
+ // for that reason. The loopback listener accepts a callback on 127.0.0.1, and the agent runs as
160
+ // the same OS user as this host, the same fact that makes file permissions useless here. So any
161
+ // local process can reach that listener, and an unverified callback would let one hand this host
162
+ // an authorization code it obtained itself. The `state` value is what makes the callback provably
163
+ // the answer to the request this host made. A mismatch is someone else talking.
164
+ //
165
+ // auth-state-mismatch the callback's `state` is not the one this host minted.
166
+ // auth-callback-refused the callback carried no authorization code: the user declined, or
167
+ // the provider returned an `error` instead.
168
+ // pkce-method-unsupported a code-challenge method other than S256 was asked for. RFC 7636
169
+ // permits `plain`; this host refuses it rather than merely not using
170
+ // it, because an unused branch is one a later reader restores.
171
+ //
172
+ // The token endpoint's two failure kinds are split for the same reason the permission decision's
173
+ // are: an outage and a shape this host does not recognise are different investigations.
174
+ //
175
+ // token-request-failed transport error, or a non-2xx this host may retry its way out of.
176
+ // token-response-invalid 2xx, and the body is not a token response this host can use.
177
+ // token-grant-rejected The grant itself is dead, and no amount of retrying fixes it.
178
+ // RFC 6749 calls this `invalid_grant`: the refresh token is expired,
179
+ // revoked, or was issued to someone else. A person has to sign in.
180
+ // It is split from `token-request-failed` because the two need
181
+ // opposite actions. A provider that is briefly unreachable wants
182
+ // patience; a grant that has lapsed wants a human, and a host that
183
+ // waits politely for one to fix itself waits forever (a refresh token
184
+ // that dies after a day of inactivity is a common provider policy).
185
+ // Some providers append their own diagnostic code to
186
+ // `error_description`; the code branches on the RFC name.
187
+ //
188
+ // The cache, and the mode check. Three outcomes rather than two, because on win32 a file written
189
+ // 0600, one chmod'ed to 0600 and a deliberately world-readable 0666 all read back as 666.
190
+ //
191
+ // credential-cache-unreadable the cache exists and could not be read or parsed.
192
+ // credential-cache-write-failed the cache could not be written.
193
+ // credential-mode-too-wide verify-after-write found the file readable by more than its
194
+ // owner on a platform that enforces modes. A real finding.
195
+ // credential-mode-unenforced A named degrade, not a failure. This filesystem records only
196
+ // the write bit, so 0600 and 0666 are indistinguishable and
197
+ // privacy cannot be confirmed. The win32 shape. "Verified after
198
+ // write" is not available here, and saying so matters, because
199
+ // the alternative is a check that passes vacuously and reads as
200
+ // proof.
201
+ // credential-mode-unobservable Inconclusive, and deliberately not the same reason. Nothing
202
+ // about a mode was observable at all, not even clearing the
203
+ // write bit, which win32 does record. So the probe cannot tell
204
+ // a filesystem that records nothing apart from a broken probe,
205
+ // and that is a thing to investigate rather than a fact about
206
+ // privacy. An instrument with no inconclusive state reports
207
+ // confidence it has not earned.
208
+ //
209
+ // device-code-not-enabled the device-code flow was reached without being configured. It is
210
+ // never a silent fallback: the provider calls it a high-risk method
211
+ // and recommends blocking it, so it must be asked for by name.
212
+ // device-code-declined the device-code flow ended without a token: declined, expired, or
213
+ // refused by the tenant.
214
+ 'identity-not-configured',
215
+ 'identity-config-invalid',
216
+ 'token-unavailable',
217
+ 'auth-state-mismatch',
218
+ 'auth-callback-refused',
219
+ 'pkce-method-unsupported',
220
+ 'token-request-failed',
221
+ 'token-response-invalid',
222
+ 'token-grant-rejected',
223
+ 'credential-cache-unreadable',
224
+ 'credential-cache-write-failed',
225
+ 'credential-mode-too-wide',
226
+ 'credential-mode-unenforced',
227
+ 'credential-mode-unobservable',
228
+ 'device-code-not-enabled',
229
+ 'device-code-declined',
230
+ // The workspace a session runs in. Two reasons, split for the same reason as the session trio
231
+ // above: provisioning failed means the session never got a directory and must not start;
232
+ // releasing failed means the work is done and something was left behind. The first blocks, the
233
+ // second is cleanup debt, and a host that reported both as one could never tell an operator
234
+ // which of the two it was looking at.
235
+ //
236
+ // workspace-provision-failed no usable directory. Never a silent fallback to a temp path or to
237
+ // the host's own cwd, which is how a session ends up writing into
238
+ // the directory the host itself is running in.
239
+ // workspace-release-failed the directory or worktree could not be released. Named rather than
240
+ // swallowed: a release that quietly fails leaks a directory per
241
+ // session, which is invisible until a disk fills.
242
+ // resume-cwd-not-honoured a resume named a directory this host's provider would not honour
243
+ // (anything but its own repository root). The CLI keeps transcripts
244
+ // per cwd, so a resume moved into a provisioned workspace finds
245
+ // nothing and becomes a fresh session that says nothing; refused by
246
+ // name instead, on the wire.
247
+ 'workspace-provision-failed',
248
+ 'workspace-release-failed',
249
+ 'resume-cwd-not-honoured',
250
+ // Tool descriptors the controller hands over, and the schemas they carry.
251
+ //
252
+ // `mcp-schema-unsupported` is the one that matters, and it exists to prevent a false green.
253
+ // A converter that met a construct it did not recognise and fell back to a permissive schema would
254
+ // register the tool successfully and validate nothing, so the property "a malformed call is
255
+ // rejected" would be silently false for exactly the tools nobody checked, while every test stayed
256
+ // green. Refusing at registration is loud, happens before any session exists, and names the
257
+ // construct. A permissive fallback is the fail-open shape this package is built against.
258
+ //
259
+ // mcp-descriptor-invalid the descriptor itself is unusable: no name, no schema, a duplicate
260
+ // name within one server.
261
+ // mcp-schema-unsupported a schema construct this host cannot convert. Refused, never widened.
262
+ // mcp-tool-input-invalid a call whose arguments failed the tool's own schema. The one of the
263
+ // three that happens at run time rather than at registration.
264
+ 'mcp-descriptor-invalid',
265
+ 'mcp-schema-unsupported',
266
+ 'mcp-tool-input-invalid',
267
+ // Durability: the transcript mirror, the transition log, and the receipt read.
268
+ //
269
+ // `receipt-anchor-unknown` is the one that matters, and it exists because the alternative is
270
+ // invisible. A delivery receipt answers "did the text land?", and the only two honest answers are
271
+ // yes and no. A third situation exists (the baseline anchor is not in the transcript at all, so
272
+ // the question cannot be evaluated) and reporting that as "no" is the failure this whole read
273
+ // path is built against: a controller that reads a false "no" pastes the text again. Naming it
274
+ // makes "could not tell" a fact code branches on rather than a silence.
275
+ //
276
+ // transcript-key-invalid an empty project key or session id, or a subpath present and
277
+ // empty. The store's key type calls an empty subpath invalid and
278
+ // says to omit the field instead, so it is refused, not coerced;
279
+ // coercing would silently address the main transcript when a
280
+ // subagent's was asked for.
281
+ // transcript-entry-malformed a stored line that will not round-trip. Round-tripping is the
282
+ // only invariant the adapter contract requires, so a line failing
283
+ // it is named rather than skipped: skipping is how a transcript
284
+ // quietly loses entries nobody counted.
285
+ // transcript-read-failed the store or the local file could not be read.
286
+ // transcript-write-failed the append did not land anywhere.
287
+ // receipt-anchor-unknown see above. Not a negative receipt.
288
+ // retention-window-invalid a retention window that is negative or not a number. Refused
289
+ // rather than defaulted, because a default here silently deletes
290
+ // on a schedule nobody chose.
291
+ // gate-cannot-grant A degrade, not a blocked call, and the only entry here that
292
+ // reports on the gate's own configuration rather than on a request.
293
+ // Raised once per gate, on the first allow issued while
294
+ // `grantOnAllow` is off: the call was approved and the tool still
295
+ // will not run, because a silent allow leaves the agent's own
296
+ // permission mode to decide and a host has nobody to answer it.
297
+ // Named because the alternative is nothing at all, on every
298
+ // approved call, for the life of the session.
299
+ 'transcript-key-invalid',
300
+ 'transcript-entry-malformed',
301
+ 'transcript-read-failed',
302
+ 'transcript-write-failed',
303
+ 'receipt-anchor-unknown',
304
+ 'retention-window-invalid',
305
+ 'gate-cannot-grant',
306
+ // The discovery door: reading the agent CLI's own transcript directory, jailed.
307
+ //
308
+ // transcript-path-escape A name that could shape a path outside the projects root. One
309
+ // reason for all three layers of the jail (the name allowlist, the
310
+ // explicit dot-name reject, and the resolve-then-containment check)
311
+ // with `detail` naming which layer refused. It is not
312
+ // `path-escapes-root`: that names the workspace jail around a
313
+ // session's own files; this names the read-only door over a directory
314
+ // the host reads on the controller's behalf. An absent transcript is
315
+ // not this: absence is a value the door reports, never a refusal.
316
+ 'transcript-path-escape',
317
+ // The configure ask: a controller changing this host's own config file over the wire.
318
+ //
319
+ // config-key-unknown An entry names a key this host does not take over the wire: a key
320
+ // outside the closed config set, or one of the keys that name the
321
+ // controller itself (the URLs, the host id). A controller re-pointing
322
+ // a host at another controller has no legitimate use, so those keys
323
+ // are refused here by name; the detail lists the settable set.
324
+ // config-value-invalid A value the host would refuse at start-up: a root that is not
325
+ // absolute, a branch scheme with no `{key}`, an unknown placeholder,
326
+ // an unmatched brace, a literal that renders an illegal branch, a
327
+ // scheme with no repository root behind it. Screened as a whole set
328
+ // before any write, so a half-applied posture never lands.
329
+ // config-host-busy The ask changes a workspace root while a session is live or
330
+ // opening. A session releases through the provider that provisioned
331
+ // it; swapping roots under one turns that release into a guess. Close
332
+ // the sessions and ask again.
333
+ // config-write-failed The file could not be written: the directory is unwritable, the
334
+ // existing file is corrupt (fix or remove it first), or this host was
335
+ // composed without a configuration seam at all.
336
+ 'config-key-unknown',
337
+ 'config-value-invalid',
338
+ 'config-host-busy',
339
+ 'config-write-failed',
340
+ // The inventory: listing the worktrees under the workspace root from disk.
341
+ //
342
+ // workspace-list-failed The host could not look: no workspace provider, a provider that
343
+ // keeps no inventory, or git refusing to list. The detail names which.
344
+ 'workspace-list-failed',
345
+ // The repository read: a controller listing a directory or reading a text file of the operator's
346
+ // checkout through this host, jailed to the repository root.
347
+ //
348
+ // repository-path-escape The asked path resolves outside the repository root, or this host
349
+ // has no repository root to read under. The same shape as
350
+ // `transcript-path-escape`, over a different root; an absent file or
351
+ // directory is not this, it is `repository-read-failed`.
352
+ // repository-read-failed The path is not a directory (for a listing) or not a file (for a
353
+ // read), the file holds a NUL byte in its head (binary; the answer is
354
+ // a string), or the filesystem refused. The detail names which.
355
+ 'repository-path-escape',
356
+ 'repository-read-failed',
357
+ // The branch half of a release: deleting a branch is not reversible, so it is refused by name
358
+ // rather than done quietly.
359
+ //
360
+ // branch-not-merged The ask said deleteBranch without force and the branch is not in
361
+ // the default branch (or no default branch exists to judge by). Nothing
362
+ // was removed: the check runs before the directory goes.
363
+ 'branch-not-merged',
364
+ ];
365
+ export function refusal(reason, detail) {
366
+ return { reason, detail };
367
+ }
368
+ /**
369
+ * The reasons a retry cannot help with: the credential is gone until a person restores it.
370
+ *
371
+ * This is a predicate and not a comment because the link's reconnect loop is correct for every
372
+ * transient failure and actively harmful for these: it dials a door that structurally cannot open,
373
+ * forever, while the one fact the operator could act on scrolls past as another retry. So "is this
374
+ * terminal" has to be a thing code branches on, and it has to have one home; a second list
375
+ * somewhere else is how the link and the reporter end up disagreeing about whether a host is dead.
376
+ *
377
+ * The set is deliberately small, and erring toward transient is the safe direction. Treating a
378
+ * recoverable failure as terminal takes down a host that would have healed; treating a terminal one
379
+ * as recoverable costs a retry loop that is loud. Only reasons whose own detail already ends in
380
+ * "sign in again" belong here.
381
+ *
382
+ * `link-unauthorized` is the third member, and it is the only one the token layer cannot raise.
383
+ * The provider judged the material and approved it; the controller refused it at the upgrade. The
384
+ * conclusion is identical (the next dial with the same identity refuses identically, and only a
385
+ * person can change what is presented), so it takes the same exit rather than a second, private
386
+ * notion of "terminal" living in the link.
387
+ *
388
+ * `credential-unavailable` is not one of them, and must never be. It is what a host with no
389
+ * identity configured reports on every dial, and that host is supposed to keep connecting without a
390
+ * header, the supported no-identity mode. Putting it here would turn the most common unconfigured
391
+ * setup into a host that refuses to start.
392
+ */
393
+ export function needsHumanReauthentication(reason) {
394
+ return (reason === 'token-grant-rejected' || reason === 'token-unavailable' || reason === 'link-unauthorized');
395
+ }
396
+ export function isRefusalReason(value) {
397
+ return REFUSAL_REASONS.includes(value);
398
+ }
399
+ //# sourceMappingURL=refusal.js.map
@@ -0,0 +1,22 @@
1
+ import type { Refusal, RefusalReason } from './refusal.js';
2
+ /**
3
+ * The return type of anything that can decline. There is no thrown-exception path for expected
4
+ * failure — a caller cannot forget to handle a `Result` the way it can forget a `try`.
5
+ */
6
+ export type Result<T> = {
7
+ readonly ok: true;
8
+ readonly value: T;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly refusal: Refusal;
12
+ };
13
+ export declare function ok<T>(value: T): Result<T>;
14
+ export declare function refuse<T>(reason: RefusalReason, detail: string): Result<T>;
15
+ /** Narrow without destructuring, for call sites that only branch. */
16
+ export declare function isOk<T>(result: Result<T>): result is {
17
+ ok: true;
18
+ value: T;
19
+ };
20
+ /** The value, or a fallback. For call sites where a refusal is genuinely uninteresting. */
21
+ export declare function valueOr<T>(result: Result<T>, fallback: T): T;
22
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { refusal } from './refusal.js';
2
+ export function ok(value) {
3
+ return { ok: true, value };
4
+ }
5
+ export function refuse(reason, detail) {
6
+ return { ok: false, refusal: refusal(reason, detail) };
7
+ }
8
+ /** Narrow without destructuring, for call sites that only branch. */
9
+ export function isOk(result) {
10
+ return result.ok;
11
+ }
12
+ /** The value, or a fallback. For call sites where a refusal is genuinely uninteresting. */
13
+ export function valueOr(result, fallback) {
14
+ return result.ok ? result.value : fallback;
15
+ }
16
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Time as an injected capability, so anything that stamps a frame is testable without waiting.
3
+ * The one real clock is built in the composition root and threaded down.
4
+ */
5
+ /** Returns the current instant as an ISO-8601 UTC string — the `at` on every frame. */
6
+ export type Clock = () => string;
7
+ /** Milliseconds since the epoch, for elapsed-time arithmetic (deadlines, backoff, heartbeat). */
8
+ export type Ticker = () => number;
9
+ export declare const systemClock: Clock;
10
+ export declare const systemTicker: Ticker;
11
+ /** A clock that advances only when told to. */
12
+ export declare function fixedClock(startMs: number): Clock & {
13
+ advance: (ms: number) => void;
14
+ };
15
+ /** A ticker that advances only when told to. Pairs with `fixedClock` for deadline tests. */
16
+ export declare function fixedTicker(startMs: number): Ticker & {
17
+ advance: (ms: number) => void;
18
+ };
19
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Time as an injected capability, so anything that stamps a frame is testable without waiting.
3
+ * The one real clock is built in the composition root and threaded down.
4
+ */
5
+ export const systemClock = () => new Date().toISOString();
6
+ export const systemTicker = () => Date.now();
7
+ /** A clock that advances only when told to. */
8
+ export function fixedClock(startMs) {
9
+ let now = startMs;
10
+ const clock = (() => new Date(now).toISOString());
11
+ clock.advance = (ms) => {
12
+ now += ms;
13
+ };
14
+ return clock;
15
+ }
16
+ /** A ticker that advances only when told to. Pairs with `fixedClock` for deadline tests. */
17
+ export function fixedTicker(startMs) {
18
+ let now = startMs;
19
+ const ticker = (() => now);
20
+ ticker.advance = (ms) => {
21
+ now += ms;
22
+ };
23
+ return ticker;
24
+ }
25
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The nouns this package is allowed to name things after — the Claude Agent SDK's own words.
3
+ *
4
+ * Every wire discriminator begins with one of these, which is what makes the naming rule
5
+ * mechanically checkable instead of a style note: a payload kind naming a concept the SDK does not
6
+ * have is a concept this layer should not be modelling.
7
+ */
8
+ export declare const SDK_NOUNS: readonly ["session", "message", "hook", "tool", "subagent", "turn", "result", "permission", "compaction"];
9
+ export type SdkNoun = (typeof SDK_NOUNS)[number];
10
+ /**
11
+ * Nouns this package owns because the SDK has no concept for them: the outbound link itself, and
12
+ * the out-of-band transfer that keeps bulk bytes off it. Kept separate from `SDK_NOUNS` so the
13
+ * distinction stays visible: these are the words this package invented rather than borrowed.
14
+ */
15
+ export declare const HOST_NOUNS: readonly ["link", "bulk"];
16
+ export type HostNoun = (typeof HOST_NOUNS)[number];
17
+ /**
18
+ * A discriminator is `<noun>_<verb>` or `<noun>/<method>`. Returns the leading noun, or null when
19
+ * the string does not start with a declared one.
20
+ */
21
+ export declare function nounOf(discriminator: string): string | null;
22
+ export declare function isDeclaredNoun(value: string): boolean;
23
+ //# sourceMappingURL=vocab.d.ts.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The nouns this package is allowed to name things after — the Claude Agent SDK's own words.
3
+ *
4
+ * Every wire discriminator begins with one of these, which is what makes the naming rule
5
+ * mechanically checkable instead of a style note: a payload kind naming a concept the SDK does not
6
+ * have is a concept this layer should not be modelling.
7
+ */
8
+ export const SDK_NOUNS = [
9
+ 'session',
10
+ 'message',
11
+ 'hook',
12
+ 'tool',
13
+ 'subagent',
14
+ 'turn',
15
+ 'result',
16
+ 'permission',
17
+ 'compaction',
18
+ ];
19
+ /**
20
+ * Nouns this package owns because the SDK has no concept for them: the outbound link itself, and
21
+ * the out-of-band transfer that keeps bulk bytes off it. Kept separate from `SDK_NOUNS` so the
22
+ * distinction stays visible: these are the words this package invented rather than borrowed.
23
+ */
24
+ export const HOST_NOUNS = ['link', 'bulk'];
25
+ const ALL_NOUNS = [...SDK_NOUNS, ...HOST_NOUNS];
26
+ /**
27
+ * A discriminator is `<noun>_<verb>` or `<noun>/<method>`. Returns the leading noun, or null when
28
+ * the string does not start with a declared one.
29
+ */
30
+ export function nounOf(discriminator) {
31
+ const head = discriminator.split(/[_/]/, 1)[0] ?? '';
32
+ return ALL_NOUNS.includes(head) ? head : null;
33
+ }
34
+ export function isDeclaredNoun(value) {
35
+ return ALL_NOUNS.includes(value);
36
+ }
37
+ //# sourceMappingURL=vocab.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The bound on a workspace id (a session key, a workspace key, a worktree's directory name).
3
+ *
4
+ * It exists so a refusal can always be sent. Every refusal echoes the key, so a key near
5
+ * `MAX_FRAME_BYTES` would make its own refusal unencodable and the named answer would degrade to
6
+ * silence. 200 keeps every echo-bearing detail far under the frame cap; a controller storing keys
7
+ * should refuse the same bound so both halves reject the same population. It lives in `core/` so
8
+ * the wire codec can enforce it on an inventory entry without the protocol closure reaching the
9
+ * workspace provider.
10
+ */
11
+ export declare const MAX_WORKSPACE_ID_LENGTH = 200;
12
+ //# sourceMappingURL=workspace-id.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The bound on a workspace id (a session key, a workspace key, a worktree's directory name).
3
+ *
4
+ * It exists so a refusal can always be sent. Every refusal echoes the key, so a key near
5
+ * `MAX_FRAME_BYTES` would make its own refusal unencodable and the named answer would degrade to
6
+ * silence. 200 keeps every echo-bearing detail far under the frame cap; a controller storing keys
7
+ * should refuse the same bound so both halves reject the same population. It lives in `core/` so
8
+ * the wire codec can enforce it on an inventory entry without the protocol closure reaching the
9
+ * workspace provider.
10
+ */
11
+ export const MAX_WORKSPACE_ID_LENGTH = 200;
12
+ //# sourceMappingURL=workspace-id.js.map