@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.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/SECURITY.md +315 -0
- package/dist/bin/command.d.ts +97 -0
- package/dist/bin/command.js +141 -0
- package/dist/bin/config.d.ts +7 -0
- package/dist/bin/config.js +90 -0
- package/dist/bin/login.d.ts +48 -0
- package/dist/bin/login.js +82 -0
- package/dist/bin/main.d.ts +33 -0
- package/dist/bin/main.js +173 -0
- package/dist/bin/pair.d.ts +28 -0
- package/dist/bin/pair.js +198 -0
- package/dist/bin/periscope.d.ts +3 -0
- package/dist/bin/periscope.js +4 -0
- package/dist/bin/posture.d.ts +64 -0
- package/dist/bin/posture.js +157 -0
- package/dist/bin/reconfigure.d.ts +64 -0
- package/dist/bin/reconfigure.js +195 -0
- package/dist/bin/serve.d.ts +103 -0
- package/dist/bin/serve.js +441 -0
- package/dist/bin/status.d.ts +11 -0
- package/dist/bin/status.js +49 -0
- package/dist/bin/workspaces.d.ts +109 -0
- package/dist/bin/workspaces.js +215 -0
- package/dist/control/backoff.d.ts +25 -0
- package/dist/control/backoff.js +20 -0
- package/dist/control/codec.d.ts +15 -0
- package/dist/control/codec.js +501 -0
- package/dist/control/credential.d.ts +23 -0
- package/dist/control/credential.js +8 -0
- package/dist/control/frames.d.ts +1180 -0
- package/dist/control/frames.js +441 -0
- package/dist/control/link-state.d.ts +32 -0
- package/dist/control/link-state.js +39 -0
- package/dist/control/link.d.ts +109 -0
- package/dist/control/link.js +736 -0
- package/dist/control/queue.d.ts +110 -0
- package/dist/control/queue.js +174 -0
- package/dist/control/seq.d.ts +49 -0
- package/dist/control/seq.js +52 -0
- package/dist/control/stream-routing.d.ts +200 -0
- package/dist/control/stream-routing.js +167 -0
- package/dist/control/stream.d.ts +43 -0
- package/dist/control/stream.js +64 -0
- package/dist/core/async-queue.d.ts +26 -0
- package/dist/core/async-queue.js +63 -0
- package/dist/core/failure.d.ts +22 -0
- package/dist/core/failure.js +53 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +7 -0
- package/dist/core/keyed-turns.d.ts +28 -0
- package/dist/core/keyed-turns.js +44 -0
- package/dist/core/paths.d.ts +25 -0
- package/dist/core/paths.js +77 -0
- package/dist/core/refusal.d.ts +43 -0
- package/dist/core/refusal.js +399 -0
- package/dist/core/result.d.ts +22 -0
- package/dist/core/result.js +16 -0
- package/dist/core/time.d.ts +19 -0
- package/dist/core/time.js +25 -0
- package/dist/core/vocab.d.ts +23 -0
- package/dist/core/vocab.js +37 -0
- package/dist/core/workspace-id.d.ts +12 -0
- package/dist/core/workspace-id.js +12 -0
- package/dist/gate/command.d.ts +136 -0
- package/dist/gate/command.js +551 -0
- package/dist/gate/decision.d.ts +111 -0
- package/dist/gate/decision.js +97 -0
- package/dist/gate/escalate.d.ts +82 -0
- package/dist/gate/escalate.js +92 -0
- package/dist/gate/gate.d.ts +173 -0
- package/dist/gate/gate.js +417 -0
- package/dist/gate/index.d.ts +39 -0
- package/dist/gate/index.js +31 -0
- package/dist/gate/jail.d.ts +92 -0
- package/dist/gate/jail.js +131 -0
- package/dist/gate/local.d.ts +69 -0
- package/dist/gate/local.js +51 -0
- package/dist/gate/outcome.d.ts +72 -0
- package/dist/gate/outcome.js +91 -0
- package/dist/gate/shell.d.ts +36 -0
- package/dist/gate/shell.js +226 -0
- package/dist/host/agent-process.d.ts +452 -0
- package/dist/host/agent-process.js +382 -0
- package/dist/host/bulk-post.d.ts +44 -0
- package/dist/host/bulk-post.js +114 -0
- package/dist/host/claude-transcripts.d.ts +84 -0
- package/dist/host/claude-transcripts.js +339 -0
- package/dist/host/config-file.d.ts +60 -0
- package/dist/host/config-file.js +216 -0
- package/dist/host/git-facts.d.ts +7 -0
- package/dist/host/git-facts.js +106 -0
- package/dist/host/hooks.d.ts +56 -0
- package/dist/host/hooks.js +75 -0
- package/dist/host/host.d.ts +356 -0
- package/dist/host/host.js +1294 -0
- package/dist/host/index.d.ts +35 -0
- package/dist/host/index.js +22 -0
- package/dist/host/link-state-file.d.ts +18 -0
- package/dist/host/link-state-file.js +66 -0
- package/dist/host/loopback.d.ts +38 -0
- package/dist/host/loopback.js +122 -0
- package/dist/host/machine.d.ts +9 -0
- package/dist/host/machine.js +19 -0
- package/dist/host/mcp-server.d.ts +11 -0
- package/dist/host/mcp-server.js +48 -0
- package/dist/host/package-facts.d.ts +3 -0
- package/dist/host/package-facts.js +26 -0
- package/dist/host/paired-credential-store.d.ts +32 -0
- package/dist/host/paired-credential-store.js +112 -0
- package/dist/host/paths.d.ts +80 -0
- package/dist/host/paths.js +165 -0
- package/dist/host/repository-read.d.ts +33 -0
- package/dist/host/repository-read.js +201 -0
- package/dist/host/session-store.d.ts +53 -0
- package/dist/host/session-store.js +100 -0
- package/dist/host/sign-in.d.ts +90 -0
- package/dist/host/sign-in.js +239 -0
- package/dist/host/telemetry.d.ts +42 -0
- package/dist/host/telemetry.js +74 -0
- package/dist/host/token-cache.d.ts +62 -0
- package/dist/host/token-cache.js +185 -0
- package/dist/host/transcript-fs.d.ts +4 -0
- package/dist/host/transcript-fs.js +104 -0
- package/dist/host/wire-request.d.ts +80 -0
- package/dist/host/wire-request.js +196 -0
- package/dist/host/workspace-fs.d.ts +14 -0
- package/dist/host/workspace-fs.js +84 -0
- package/dist/host/workspace-trust.d.ts +23 -0
- package/dist/host/workspace-trust.js +80 -0
- package/dist/identity/authorize.d.ts +59 -0
- package/dist/identity/authorize.js +72 -0
- package/dist/identity/config.d.ts +98 -0
- package/dist/identity/config.js +130 -0
- package/dist/identity/credential.d.ts +91 -0
- package/dist/identity/credential.js +76 -0
- package/dist/identity/device-code.d.ts +115 -0
- package/dist/identity/device-code.js +134 -0
- package/dist/identity/index.d.ts +33 -0
- package/dist/identity/index.js +10 -0
- package/dist/identity/mode.d.ts +106 -0
- package/dist/identity/mode.js +69 -0
- package/dist/identity/paired-credential.d.ts +52 -0
- package/dist/identity/paired-credential.js +43 -0
- package/dist/identity/pkce.d.ts +50 -0
- package/dist/identity/pkce.js +94 -0
- package/dist/identity/store.d.ts +43 -0
- package/dist/identity/store.js +43 -0
- package/dist/identity/token.d.ts +58 -0
- package/dist/identity/token.js +149 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +53 -0
- package/dist/mcp/descriptor.d.ts +59 -0
- package/dist/mcp/descriptor.js +14 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/schema.d.ts +37 -0
- package/dist/mcp/schema.js +175 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/server.js +66 -0
- package/dist/persistence/entry.d.ts +74 -0
- package/dist/persistence/entry.js +105 -0
- package/dist/persistence/index.d.ts +22 -0
- package/dist/persistence/index.js +8 -0
- package/dist/persistence/key.d.ts +46 -0
- package/dist/persistence/key.js +33 -0
- package/dist/persistence/mirror.d.ts +71 -0
- package/dist/persistence/mirror.js +57 -0
- package/dist/persistence/receipt.d.ts +111 -0
- package/dist/persistence/receipt.js +85 -0
- package/dist/persistence/retention.d.ts +68 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/store.d.ts +106 -0
- package/dist/persistence/store.js +86 -0
- package/dist/persistence/transition-log.d.ts +73 -0
- package/dist/persistence/transition-log.js +133 -0
- package/dist/protocol.d.ts +27 -0
- package/dist/protocol.js +12 -0
- package/dist/sessions/index.d.ts +8 -0
- package/dist/sessions/index.js +4 -0
- package/dist/sessions/registry.d.ts +186 -0
- package/dist/sessions/registry.js +190 -0
- package/dist/sessions/session.d.ts +178 -0
- package/dist/sessions/session.js +288 -0
- package/dist/sessions/spawn-env.d.ts +88 -0
- package/dist/sessions/spawn-env.js +277 -0
- package/dist/state/coverage.d.ts +308 -0
- package/dist/state/coverage.js +315 -0
- package/dist/state/index.d.ts +12 -0
- package/dist/state/index.js +7 -0
- package/dist/state/machine.d.ts +112 -0
- package/dist/state/machine.js +237 -0
- package/dist/state/model.d.ts +233 -0
- package/dist/state/model.js +214 -0
- package/dist/state/observer.d.ts +55 -0
- package/dist/state/observer.js +413 -0
- package/dist/state/reporter.d.ts +45 -0
- package/dist/state/reporter.js +35 -0
- package/dist/state/store.d.ts +56 -0
- package/dist/state/store.js +120 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry/usage.d.ts +78 -0
- package/dist/telemetry/usage.js +69 -0
- package/dist/workspace/git-worktree.d.ts +150 -0
- package/dist/workspace/git-worktree.js +417 -0
- package/dist/workspace/index.d.ts +9 -0
- package/dist/workspace/index.js +4 -0
- package/dist/workspace/plain-dir.d.ts +34 -0
- package/dist/workspace/plain-dir.js +90 -0
- package/dist/workspace/provider.d.ts +152 -0
- package/dist/workspace/provider.js +2 -0
- package/dist/workspace/worktree-porcelain.d.ts +29 -0
- package/dist/workspace/worktree-porcelain.js +100 -0
- package/docs/architecture.md +277 -0
- package/docs/configuration.md +187 -0
- package/docs/gate.md +219 -0
- package/docs/identity.md +107 -0
- package/docs/protocol.md +501 -0
- package/docs/state-machine.md +160 -0
- package/examples/README.md +141 -0
- package/examples/minimal-controller/controller.ts +38 -0
- package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
- package/examples/parallel-run-proof/run.ts +850 -0
- package/examples/test-controller/controller.ts +655 -0
- package/examples/test-controller/serve.ts +29 -0
- package/examples/tsconfig.json +30 -0
- package/package.json +98 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent SDK seam: the only module in this package that starts an agent process, and the one
|
|
3
|
+
* that re-exports the SDK's types for every layer above.
|
|
4
|
+
*
|
|
5
|
+
* It is not the only file that names `@anthropic-ai/claude-agent-sdk`. Three others in
|
|
6
|
+
* `src/host/` do (the MCP server builder, the store adapter and the telemetry reader), because
|
|
7
|
+
* each bridges one SDK shape and none of them spawns anything. The rule the pin enforces is the
|
|
8
|
+
* directory, not this file; what remains unique here is `query()`.
|
|
9
|
+
*
|
|
10
|
+
* It lives in `src/host/` because calling `query()` spawns a real CLI subprocess. The boundary this
|
|
11
|
+
* directory holds is stated as "nothing outside `src/host/` touches the filesystem, spawns a
|
|
12
|
+
* process, or reads the machine", and a package import that spawns is exactly as privileged as
|
|
13
|
+
* `node:child_process` even though no lint rule would have noticed. Pinned by
|
|
14
|
+
* src/pins/sdk-confinement.test.ts.
|
|
15
|
+
*
|
|
16
|
+
* Everything above this file works in the package's own vocabulary and never imports the SDK, so
|
|
17
|
+
* "what starts a process, and with what?" has one answer. The SDK types the layer above genuinely
|
|
18
|
+
* needs are re-exported here rather than imported there; a type-only import would still name the
|
|
19
|
+
* specifier, and one place naming it is the whole point.
|
|
20
|
+
*
|
|
21
|
+
* `windowsHide` is not a knob this package has, and it does not need to be. Node's default is
|
|
22
|
+
* `false` (the widespread "true since Node 15" belief is wrong), and where no ancestor process owns
|
|
23
|
+
* a console there is none to inherit, so every un-hidden spawn allocates a real visible window. It
|
|
24
|
+
* is absent from the SDK's `Options`, but the SDK's own `spawnLocalProcess` passes
|
|
25
|
+
* `windowsHide: true`, so the default path is correct. The trap is `spawnClaudeCodeProcess`: a
|
|
26
|
+
* custom spawn function replaces that path entirely, and one written without `windowsHide`
|
|
27
|
+
* reintroduces a flashing window on every spawn.
|
|
28
|
+
*/
|
|
29
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
30
|
+
import { AsyncQueue } from '../core/async-queue.js';
|
|
31
|
+
/** The discriminator of one message, in the same form `MessageDiscriminator` names. */
|
|
32
|
+
export function discriminatorOf(message) {
|
|
33
|
+
return message.type === 'system' ? `system/${message.subtype}` : message.type;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every key `Options` is composed from, as data.
|
|
37
|
+
*
|
|
38
|
+
* This is the permission-config pin's subject, and it is why that pin is a compile error rather
|
|
39
|
+
* than a grep. The SDK's `Options` carries nine lanes that alter permission outcomes:
|
|
40
|
+
* `permissionMode`, `settings`, `managedSettings`, `toolAliases`, `permissionPromptToolName`,
|
|
41
|
+
* `allowedTools`, `disallowedTools`, `canUseTool`, and the `permissions` block a settings object can
|
|
42
|
+
* carry. Eight of them (`SHADOWING_LANES`) are unreachable, because a caller can only supply the
|
|
43
|
+
* keys below and `startAgentProcess` composes `Options` from exactly these; `permissionMode` is the
|
|
44
|
+
* one opened by name, in `CLI_PARITY_OPTION_KEYS`. Adding a composable option breaks this
|
|
45
|
+
* declaration, and the pin fails at build time instead of when someone remembers to look.
|
|
46
|
+
*
|
|
47
|
+
* The composable set is declared as several lists, each with its own reason, because the moment two
|
|
48
|
+
* different justifications share one constant, neither can be checked:
|
|
49
|
+
*
|
|
50
|
+
* - `STREAMING_OPTION_KEYS` select what the process emits, never what it may do. A widening for
|
|
51
|
+
* visibility is not a weakening for permission.
|
|
52
|
+
* - `TOOL_SURFACE_OPTION_KEYS` change which tools exist. Read that list's note for why it is
|
|
53
|
+
* nonetheless safe.
|
|
54
|
+
* - `PERSISTENCE_OPTION_KEYS` decide where the transcript goes: the egress lane. Its safety
|
|
55
|
+
* argument is structural and is pinned rather than stated.
|
|
56
|
+
* - `AGENT_SELECTION_OPTION_KEYS` choose which agent runs and what it is told at the start.
|
|
57
|
+
* - `CLI_PARITY_OPTION_KEYS` expose what the CLI itself exposes, including the one shadowing lane
|
|
58
|
+
* that is a posture rather than a rule file.
|
|
59
|
+
*
|
|
60
|
+
* The distinction that decides whether a widening is a weakening: `model` and `systemPrompt` were
|
|
61
|
+
* absent, not narrowed. They were never among the composable keys, so opening them filled a gap.
|
|
62
|
+
* The eight lanes in `SHADOWING_LANES` are a deliberate security narrowing and stay closed, and the
|
|
63
|
+
* pin still asserts each one by name. A gap filled and a narrowing widened are different acts and
|
|
64
|
+
* this file tells them apart.
|
|
65
|
+
*
|
|
66
|
+
* `toolAliases` is the one worth naming: it redirects tool names after the model emits them, so a
|
|
67
|
+
* gate matching on `tool_name` would see the alias source while the target executed. A silent
|
|
68
|
+
* mismatch, and unreachable here by construction.
|
|
69
|
+
*/
|
|
70
|
+
export const AGENT_PROCESS_REQUEST_KEYS = {
|
|
71
|
+
cwd: true,
|
|
72
|
+
env: true,
|
|
73
|
+
settingSources: true,
|
|
74
|
+
plugins: true,
|
|
75
|
+
hooks: true,
|
|
76
|
+
resume: true,
|
|
77
|
+
fork: true,
|
|
78
|
+
onStderr: true,
|
|
79
|
+
spawn: true,
|
|
80
|
+
includePartialMessages: true,
|
|
81
|
+
thinking: true,
|
|
82
|
+
forwardSubagentText: true,
|
|
83
|
+
mcpServers: true,
|
|
84
|
+
strictMcpConfig: true,
|
|
85
|
+
sessionStore: true,
|
|
86
|
+
sessionStoreFlush: true,
|
|
87
|
+
model: true,
|
|
88
|
+
systemPrompt: true,
|
|
89
|
+
effort: true,
|
|
90
|
+
permissionMode: true,
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The eight closed lanes, as data: the subject both permission pins are about.
|
|
94
|
+
*
|
|
95
|
+
* The defect this guards against: the lanes were once written out twice, once per pin, and the two
|
|
96
|
+
* copies disagreed. The scan pin listed `permissionPrompt`, which does not exist in `sdk.d.ts` at
|
|
97
|
+
* all, and omitted `permissions`, which does. So the "two mechanisms, one invariant" argument (a
|
|
98
|
+
* type cannot see a module that reaches past the composer, a scan cannot see a type) was being made
|
|
99
|
+
* by two lists guarding different sets, with the real option covered by only one of them.
|
|
100
|
+
*
|
|
101
|
+
* One declaration now, derived by both, and every name checked against the shipped types.
|
|
102
|
+
*/
|
|
103
|
+
export const SHADOWING_LANES = [
|
|
104
|
+
// `permissionMode` was deliberately removed from this list (CLI parity); see
|
|
105
|
+
// `CLI_PARITY_OPTION_KEYS`. It is the one lane that is a posture the operator chooses in the open,
|
|
106
|
+
// not a rule file or a pre-answer; the eight below are the latter and stay closed.
|
|
107
|
+
'settings',
|
|
108
|
+
'managedSettings',
|
|
109
|
+
'toolAliases',
|
|
110
|
+
'permissionPromptToolName',
|
|
111
|
+
'allowedTools',
|
|
112
|
+
'disallowedTools',
|
|
113
|
+
'canUseTool',
|
|
114
|
+
'permissions',
|
|
115
|
+
];
|
|
116
|
+
/**
|
|
117
|
+
* The keys that widened the composable set for visibility, kept as data so the reason is checkable
|
|
118
|
+
* rather than remembered.
|
|
119
|
+
*
|
|
120
|
+
* One reason covers all three: each selects what the process emits. None appears in any permission
|
|
121
|
+
* evaluation path, so none can change whether a tool runs, only how much of the run is visible.
|
|
122
|
+
* That is why widening the set here does not weaken the boundary above.
|
|
123
|
+
*
|
|
124
|
+
* The pin asserts every member is composable and is none of the eight shadowing lanes, so a later
|
|
125
|
+
* addition cannot join this list by assertion alone.
|
|
126
|
+
*/
|
|
127
|
+
export const STREAMING_OPTION_KEYS = [
|
|
128
|
+
'includePartialMessages',
|
|
129
|
+
'thinking',
|
|
130
|
+
'forwardSubagentText',
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* The keys that change which tools exist: a different class from the streaming three, kept
|
|
134
|
+
* separate so the two reasons cannot be confused for one.
|
|
135
|
+
*
|
|
136
|
+
* These are not data keys. `mcpServers` introduces tools, and `PreToolUse` fires for MCP tools and
|
|
137
|
+
* inside subagents (observed, not assumed). So this widening enlarges the surface the gate must
|
|
138
|
+
* cover, and the streaming keys' reason ("it only selects what is emitted") is false of it.
|
|
139
|
+
*
|
|
140
|
+
* Nor are they shadowing lanes. The closed lanes share one property: they can answer a permission
|
|
141
|
+
* before the gate does. These answer nothing. What makes them safe is structural: `permissionHooks`
|
|
142
|
+
* registers `PreToolUse` with no `matcher`, so a tool this host has never heard of reaches exactly
|
|
143
|
+
* the same handler as `Bash`. Coverage is by construction rather than by enumeration, which is why a
|
|
144
|
+
* tool set the host cannot predict is still a tool set the host decides about.
|
|
145
|
+
*
|
|
146
|
+
* If a `matcher` were ever introduced, this classification stops being true, and these two keys
|
|
147
|
+
* become the first way to add a tool nothing decides about. That sentence is the whole reason this
|
|
148
|
+
* list exists as data instead of as a decision somebody made once.
|
|
149
|
+
*
|
|
150
|
+
* `strictMcpConfig` belongs here because it acts on the same surface, but it moves the opposite
|
|
151
|
+
* way: it only ever removes servers the host did not declare. It is the safe direction of the same
|
|
152
|
+
* lane, and grouping them keeps that visible.
|
|
153
|
+
*/
|
|
154
|
+
export const TOOL_SURFACE_OPTION_KEYS = [
|
|
155
|
+
'mcpServers',
|
|
156
|
+
'strictMcpConfig',
|
|
157
|
+
];
|
|
158
|
+
/**
|
|
159
|
+
* The keys that decide where a transcript goes: a third class again, and the reason is not the
|
|
160
|
+
* other two lists' reason.
|
|
161
|
+
*
|
|
162
|
+
* These are a confidentiality and egress surface. The streaming keys widen what this host observes
|
|
163
|
+
* about its own session. The tool-surface keys widen what the agent may do, and are covered because
|
|
164
|
+
* the gate has no matcher. These do neither: they hand a live object every message the agent saw,
|
|
165
|
+
* and a store is by definition somewhere else. Filing them under "it only selects what is emitted"
|
|
166
|
+
* would put a false sentence inside the constant whose entire job is to make the reason checkable.
|
|
167
|
+
*
|
|
168
|
+
* Nor are they shadowing lanes. The closed lanes share one property: they can answer a permission
|
|
169
|
+
* before the gate does. A store answers nothing and is never consulted about whether a tool runs.
|
|
170
|
+
*
|
|
171
|
+
* What makes them safe is that a destination cannot be named from off-box, and it is structural
|
|
172
|
+
* rather than enforced: `SessionStore` is an object with methods, so it has no JSON representation
|
|
173
|
+
* and cannot arrive over the link. The only code that can supply one is the code that composes this
|
|
174
|
+
* host. That is the same shape of argument as the gate's no-matcher coverage (a property of what
|
|
175
|
+
* the type is, not of a check somebody remembered to write) and it is pinned by
|
|
176
|
+
* pins/persistence-egress.test.ts.
|
|
177
|
+
*
|
|
178
|
+
* If a store ever becomes constructible from data (a URL, a connection string, a descriptor the
|
|
179
|
+
* host resolves into a client), this classification stops being true, and these become the first
|
|
180
|
+
* way a transcript can be sent somewhere the operator did not choose. That sentence is why this list
|
|
181
|
+
* is data instead of a decision somebody made once.
|
|
182
|
+
*/
|
|
183
|
+
export const PERSISTENCE_OPTION_KEYS = [
|
|
184
|
+
'sessionStore',
|
|
185
|
+
'sessionStoreFlush',
|
|
186
|
+
];
|
|
187
|
+
/**
|
|
188
|
+
* The keys that choose which agent runs and what it is told at the start: a fourth class, and its
|
|
189
|
+
* reason is not any of the other three's.
|
|
190
|
+
*
|
|
191
|
+
* These were absent, not narrowed, and that is the whole classification. `model` and
|
|
192
|
+
* `systemPrompt` were simply not in this type, so nothing was ever protecting them; there was no
|
|
193
|
+
* decision to reverse, only a capability nobody had wired. The eight `SHADOWING_LANES` are the
|
|
194
|
+
* opposite case: each was considered and closed. Opening a gap and re-opening a closed lane look
|
|
195
|
+
* identical in a diff, and this list is how they stop looking identical.
|
|
196
|
+
*
|
|
197
|
+
* Neither appears in any permission evaluation path. They do not pre-answer a call, do not
|
|
198
|
+
* redirect a tool name, do not load a rule file and are never consulted about whether a tool runs,
|
|
199
|
+
* which is exactly the property the closed lanes share and these do not. What they change is which
|
|
200
|
+
* weights answer and what standing instructions those weights start with.
|
|
201
|
+
*
|
|
202
|
+
* The cost, because a widening with no stated cost is a widening nobody checked: a controller that
|
|
203
|
+
* can set a system prompt can give the agent standing instructions this host will never see the
|
|
204
|
+
* effect of, since the SDK's init message does not report one. The gate still decides every tool
|
|
205
|
+
* call, so the boundary is unmoved, but "the host can state what this agent was told" is not a
|
|
206
|
+
* property this package has; `AgentInitFacts` is where that can be verified.
|
|
207
|
+
*
|
|
208
|
+
* The pin asserts every member is composable and is none of the eight, exactly as the other lists
|
|
209
|
+
* do.
|
|
210
|
+
*/
|
|
211
|
+
export const AGENT_SELECTION_OPTION_KEYS = [
|
|
212
|
+
'model',
|
|
213
|
+
'systemPrompt',
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* The CLI-parity keys, and the list that re-opens a closed lane on purpose. `effort` was absent (a
|
|
217
|
+
* gap, like `model`). `permissionMode` was once a shadowing lane, considered and closed, and it is
|
|
218
|
+
* opened here by name, with the reason:
|
|
219
|
+
*
|
|
220
|
+
* a session through this host must expose what `claude` exposes, and bypass is a common default
|
|
221
|
+
* for an operator. The gate never depended on the mode: `PreToolUse` fires under every mode, so
|
|
222
|
+
* the boundary set is still held under `bypassPermissions`. What the mode changes is the CLI's
|
|
223
|
+
* own prompt flow, which this host's gate already answers.
|
|
224
|
+
*
|
|
225
|
+
* The other eight stay closed: they are rule files and pre-answers, which is a different thing from a
|
|
226
|
+
* posture chosen in the open. Pinned by `pins/permission-config.test.ts`.
|
|
227
|
+
*/
|
|
228
|
+
export const CLI_PARITY_OPTION_KEYS = [
|
|
229
|
+
'effort',
|
|
230
|
+
'permissionMode',
|
|
231
|
+
];
|
|
232
|
+
/**
|
|
233
|
+
* The init message's facts, or null for every other message.
|
|
234
|
+
*
|
|
235
|
+
* Reading it here rather than above the seam keeps the SDK's message union on this side; the layer
|
|
236
|
+
* above branches on a plain record.
|
|
237
|
+
*/
|
|
238
|
+
export function readInitFacts(message) {
|
|
239
|
+
if (message.type !== 'system' || message.subtype !== 'init')
|
|
240
|
+
return null;
|
|
241
|
+
return {
|
|
242
|
+
sessionId: message.session_id,
|
|
243
|
+
cliVersion: message.claude_code_version,
|
|
244
|
+
cwd: message.cwd,
|
|
245
|
+
model: message.model,
|
|
246
|
+
permissionMode: message.permissionMode,
|
|
247
|
+
apiKeySource: message.apiKeySource,
|
|
248
|
+
tools: message.tools,
|
|
249
|
+
skills: message.skills,
|
|
250
|
+
// Guarded for the same reason as `mcp_servers` below: `plugins` is declared required and read
|
|
251
|
+
// with `.map(…)`, the identical shape, the identical runtime-wins argument, the identical
|
|
252
|
+
// `TypeError`. Every member of this class is guarded, not only the one observed failing.
|
|
253
|
+
plugins: (message.plugins ?? []).map((plugin) => ({
|
|
254
|
+
name: plugin.name,
|
|
255
|
+
path: plugin.path,
|
|
256
|
+
version: plugin.version ?? null,
|
|
257
|
+
})),
|
|
258
|
+
capabilities: message.capabilities ?? [],
|
|
259
|
+
// Guarded though the type says required. `mcp_servers` is declared non-optional on the init
|
|
260
|
+
// message, so `message.mcp_servers.map(…)` typechecks, and this package's rule is that the
|
|
261
|
+
// runtime wins over the types. An init message without the field would throw a TypeError inside
|
|
262
|
+
// `readInitFacts`, and the pump's own catch would convert the throw into `process_failed`: the
|
|
263
|
+
// session ends, and the reader's bug is reported as the agent process dying. `capabilities` one
|
|
264
|
+
// line up is guarded for the same reason, and it at least has `?` in the type as a warning.
|
|
265
|
+
//
|
|
266
|
+
// The cost of an unguarded read is misattribution, not silence. This read has exactly one
|
|
267
|
+
// caller, `HostedSession.#pump`, which calls it before and outside the per-listener try/catch,
|
|
268
|
+
// so nothing swallows it: the enclosing catch finishes the session as `process_failed`, closes
|
|
269
|
+
// the process, releases the registry entry, refuses every `whenLive` waiter and puts a
|
|
270
|
+
// transition to `ended` on the wire. `state/observer.ts` contains no catch at all, so no
|
|
271
|
+
// observer wrapper can eat the throw.
|
|
272
|
+
mcpServers: (message.mcp_servers ?? []).map((server) => ({ name: server.name, status: server.status })),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The message stream, and ONLY the message stream.
|
|
277
|
+
*
|
|
278
|
+
* `return` and `throw` are delegated, not just `next`. `for await…of` calls `iterator.return()`
|
|
279
|
+
* when the loop leaves early — a `break`, a `return`, or a throw inside the body — and that call is
|
|
280
|
+
* what lets the underlying query release the subprocess. A wrapper implementing only `next` would
|
|
281
|
+
* swallow it, leaking a session per abandoned loop with no error, no log and nothing null: exactly
|
|
282
|
+
* the silent-loss shape this package is built against. Pinned by agent-process.test.ts.
|
|
283
|
+
*
|
|
284
|
+
* Exported for that pin. It is not part of the package's public surface.
|
|
285
|
+
*/
|
|
286
|
+
export function messagesOf(source) {
|
|
287
|
+
return {
|
|
288
|
+
next: (...args) => source.next(...args),
|
|
289
|
+
return: (value) => source.return(value),
|
|
290
|
+
throw: (error) => source.throw(error),
|
|
291
|
+
[Symbol.asyncIterator]() {
|
|
292
|
+
return this;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Start an agent process. The subprocess exists when this returns. */
|
|
297
|
+
export function startAgentProcess(request) {
|
|
298
|
+
const input = new AsyncQueue();
|
|
299
|
+
const options = {
|
|
300
|
+
cwd: request.cwd,
|
|
301
|
+
env: request.env,
|
|
302
|
+
settingSources: [...request.settingSources],
|
|
303
|
+
includePartialMessages: request.includePartialMessages,
|
|
304
|
+
forwardSubagentText: request.forwardSubagentText,
|
|
305
|
+
...(request.thinking === null ? {} : { thinking: request.thinking }),
|
|
306
|
+
...(request.mcpServers === null ? {} : { mcpServers: { ...request.mcpServers } }),
|
|
307
|
+
...(request.strictMcpConfig === null ? {} : { strictMcpConfig: request.strictMcpConfig }),
|
|
308
|
+
// `persistSession` is deliberately never composed: the mirror runs after the local write, so a
|
|
309
|
+
// store cannot be combined with local persistence off. Leaving the SDK's default in place is
|
|
310
|
+
// what makes that combination unbuildable here rather than merely undocumented.
|
|
311
|
+
...(request.sessionStore === null ? {} : { sessionStore: request.sessionStore }),
|
|
312
|
+
...(request.sessionStoreFlush === null ? {} : { sessionStoreFlush: request.sessionStoreFlush }),
|
|
313
|
+
...(request.plugins === null ? {} : { plugins: [...request.plugins] }),
|
|
314
|
+
...(request.hooks === null ? {} : { hooks: request.hooks }),
|
|
315
|
+
...(request.resume === null ? {} : { resume: request.resume, forkSession: request.fork }),
|
|
316
|
+
...(request.onStderr === null ? {} : { stderr: request.onStderr }),
|
|
317
|
+
...(request.spawn === null ? {} : { spawnClaudeCodeProcess: request.spawn }),
|
|
318
|
+
// Omitted rather than passed as null when unset, like every other optional above: the SDK reads
|
|
319
|
+
// an absent key as "use the default" and a present-but-null one as a value it must interpret.
|
|
320
|
+
...(request.model === null ? {} : { model: request.model }),
|
|
321
|
+
...(request.systemPrompt === null ? {} : { systemPrompt: request.systemPrompt }),
|
|
322
|
+
...(request.effort === null ? {} : { effort: request.effort }),
|
|
323
|
+
...(request.permissionMode === null ? {} : { permissionMode: request.permissionMode }),
|
|
324
|
+
};
|
|
325
|
+
const running = query({ prompt: input, options });
|
|
326
|
+
let closed = false;
|
|
327
|
+
return {
|
|
328
|
+
messages: messagesOf(running),
|
|
329
|
+
prompt(text) {
|
|
330
|
+
if (closed)
|
|
331
|
+
return false;
|
|
332
|
+
// `session_id` and `uuid` are optional on this type and deliberately left off: the agent
|
|
333
|
+
// stamps its own. Sending an empty string would be a PRESENT id that is blank, which is a
|
|
334
|
+
// different and worse thing than an absent one.
|
|
335
|
+
const message = {
|
|
336
|
+
type: 'user',
|
|
337
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
338
|
+
parent_tool_use_id: null,
|
|
339
|
+
};
|
|
340
|
+
input.push(message);
|
|
341
|
+
return true;
|
|
342
|
+
},
|
|
343
|
+
async interrupt() {
|
|
344
|
+
if (closed)
|
|
345
|
+
return;
|
|
346
|
+
// The SDK's receipt names which queued messages survived the interrupt. Not surfaced here:
|
|
347
|
+
// acting on it needs the turn model, which belongs to the layer that owns turns.
|
|
348
|
+
await running.interrupt();
|
|
349
|
+
},
|
|
350
|
+
async setModel(model) {
|
|
351
|
+
if (closed)
|
|
352
|
+
return;
|
|
353
|
+
// `undefined`, never null: the SDK reads undefined as "the default" and null as a model named null.
|
|
354
|
+
await running.setModel(model ?? undefined);
|
|
355
|
+
},
|
|
356
|
+
async setPermissionMode(mode) {
|
|
357
|
+
if (closed)
|
|
358
|
+
return;
|
|
359
|
+
await running.setPermissionMode(mode);
|
|
360
|
+
},
|
|
361
|
+
async setThinking(thinking) {
|
|
362
|
+
if (closed)
|
|
363
|
+
return;
|
|
364
|
+
// The SDK's LIVE setter is the token cap, and on current models it is on/off: 0 = disabled,
|
|
365
|
+
// null = the default (adaptive). A fixed budget is REJECTED by Opus 5 / Sonnet 5 / Fable, so an
|
|
366
|
+
// `enabled` ask maps to adaptive rather than to a 400. The DISPLAY rides along: `summarized` when
|
|
367
|
+
// asked, because the models' default (`omitted`) streams thinking blocks with empty text — the
|
|
368
|
+
// "no thinking" an operator sees while paying for it.
|
|
369
|
+
const cap = thinking.type === 'disabled' ? 0 : null;
|
|
370
|
+
const display = thinking.type === 'disabled' ? undefined : thinking.display;
|
|
371
|
+
await running.setMaxThinkingTokens(cap, display);
|
|
372
|
+
},
|
|
373
|
+
close() {
|
|
374
|
+
if (closed)
|
|
375
|
+
return;
|
|
376
|
+
closed = true;
|
|
377
|
+
input.end();
|
|
378
|
+
running.close();
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=agent-process.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Result } from '../core/result.js';
|
|
2
|
+
export interface BulkPostRequest {
|
|
3
|
+
readonly deliveryId: string;
|
|
4
|
+
readonly postUrl: string;
|
|
5
|
+
/**
|
|
6
|
+
* The only origin this delivery may reach. Required, with no default, and that is the mechanism
|
|
7
|
+
* rather than a style.
|
|
8
|
+
*
|
|
9
|
+
* `postUrl` is chosen by the peer and arrives verbatim on the wire; this is the value that says
|
|
10
|
+
* which peer the host actually works for. An optional field defaulting to "no binding" would let
|
|
11
|
+
* every present and future caller inherit a fail-open nobody picked. Required, the compiler
|
|
12
|
+
* forces each caller to name the origin it trusts.
|
|
13
|
+
*
|
|
14
|
+
* Compute it with {@link bulkOriginFor}, which is the only thing that knows how to turn the
|
|
15
|
+
* host's configured `ws(s)://` controller URL into the `http(s)://` origin a POST lands on.
|
|
16
|
+
*/
|
|
17
|
+
readonly allowedOrigin: string;
|
|
18
|
+
/** Absolute. A relative path means "relative to a cwd the controller cannot see". */
|
|
19
|
+
readonly filePath: string;
|
|
20
|
+
readonly fromOffset: number;
|
|
21
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The host's ONE controller origin, derived from the link URL it already dials.
|
|
25
|
+
*
|
|
26
|
+
* Derived, never separately configured. The host dials `controllerUrl` and trusts whatever
|
|
27
|
+
* answers it; the bulk POST goes to the same deployment over plain HTTP. Reading a second setting
|
|
28
|
+
* would create a way for the two to disagree, and a host whose "trusted origin" is configured apart
|
|
29
|
+
* from the one it actually talks to is a host that can be pointed at two peers at once.
|
|
30
|
+
*/
|
|
31
|
+
export declare function bulkOriginFor(controllerUrl: string): Result<string>;
|
|
32
|
+
export interface BulkPostReceipt {
|
|
33
|
+
readonly deliveryId: string;
|
|
34
|
+
readonly byteCount: number;
|
|
35
|
+
/**
|
|
36
|
+
* The file's stat at the moment the delivery was read (size total, mtime as floored epoch ms).
|
|
37
|
+
* Carried so a transcript puller can detect the CLI rewriting the file under it — a byte-offset
|
|
38
|
+
* resume across a rewrite is invalid, and only the deliverer's own stat can say.
|
|
39
|
+
*/
|
|
40
|
+
readonly sizeBytes: number;
|
|
41
|
+
readonly mtimeMs: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function postBulk(request: BulkPostRequest): Promise<Result<BulkPostReceipt>>;
|
|
44
|
+
//# sourceMappingURL=bulk-post.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host half of the bulk lane: bytes leave over an outbound HTTP POST, never over the link.
|
|
3
|
+
*
|
|
4
|
+
* The link carries a locator and gets back a receipt; the content travels here. That inversion is
|
|
5
|
+
* what keeps a megabyte transcript off a socket that also carries the transitions telling you what
|
|
6
|
+
* a session is doing — a single large frame would otherwise stall every session sharing the link.
|
|
7
|
+
*
|
|
8
|
+
* The read is streamed. Buffering a whole transcript to send it would block the event loop for
|
|
9
|
+
* every other session this host is serving, which is the same failure in a different costume.
|
|
10
|
+
*/
|
|
11
|
+
import { createReadStream } from 'node:fs';
|
|
12
|
+
import { stat } from 'node:fs/promises';
|
|
13
|
+
import { Readable } from 'node:stream';
|
|
14
|
+
import { ok, refuse } from '../core/result.js';
|
|
15
|
+
import { requireAbsolute } from '../core/paths.js';
|
|
16
|
+
/**
|
|
17
|
+
* The host's ONE controller origin, derived from the link URL it already dials.
|
|
18
|
+
*
|
|
19
|
+
* Derived, never separately configured. The host dials `controllerUrl` and trusts whatever
|
|
20
|
+
* answers it; the bulk POST goes to the same deployment over plain HTTP. Reading a second setting
|
|
21
|
+
* would create a way for the two to disagree, and a host whose "trusted origin" is configured apart
|
|
22
|
+
* from the one it actually talks to is a host that can be pointed at two peers at once.
|
|
23
|
+
*/
|
|
24
|
+
export function bulkOriginFor(controllerUrl) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(controllerUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return refuse('bulk-target-invalid', `the configured controller URL is not a URL: ${controllerUrl}`);
|
|
31
|
+
}
|
|
32
|
+
// The link speaks WebSocket and the bulk lane speaks HTTP against the SAME deployment, so the
|
|
33
|
+
// scheme is mapped rather than compared: `wss://host:8443/periscope/link` and
|
|
34
|
+
// `https://host:8443/periscope/bulk/<id>` are one controller, and `URL.origin` alone would call
|
|
35
|
+
// them different peers.
|
|
36
|
+
const scheme = parsed.protocol === 'ws:' ? 'http:' : parsed.protocol === 'wss:' ? 'https:' : parsed.protocol;
|
|
37
|
+
if (scheme !== 'http:' && scheme !== 'https:') {
|
|
38
|
+
return refuse('bulk-target-invalid', `a controller URL cannot carry scheme ${parsed.protocol}`);
|
|
39
|
+
}
|
|
40
|
+
return ok(`${scheme}//${parsed.host}`);
|
|
41
|
+
}
|
|
42
|
+
export async function postBulk(request) {
|
|
43
|
+
const absolute = requireAbsolute(request.filePath);
|
|
44
|
+
if (!absolute.ok)
|
|
45
|
+
return { ok: false, refusal: absolute.refusal };
|
|
46
|
+
let target;
|
|
47
|
+
try {
|
|
48
|
+
target = new URL(request.postUrl);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return refuse('bulk-target-invalid', `not a URL: ${request.postUrl}`);
|
|
52
|
+
}
|
|
53
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') {
|
|
54
|
+
return refuse('bulk-target-invalid', `unsupported scheme: ${target.protocol}`);
|
|
55
|
+
}
|
|
56
|
+
// The destination is the peer's choice; whether it is reached is not.
|
|
57
|
+
//
|
|
58
|
+
// The caller attaches this host's durable, non-expiring paired credential to the request (see
|
|
59
|
+
// `host.ts`'s `#deliver`). With a credential riding along, a peer-named `postUrl` is an
|
|
60
|
+
// exfiltration surface: any code path that can compose a `BulkRequest` could name
|
|
61
|
+
// `http://attacker.example/x` and receive the operator's transcript and the device credential
|
|
62
|
+
// that outlives the discovery of the theft.
|
|
63
|
+
//
|
|
64
|
+
// Checked after the scheme, on purpose. A garbled URL should still say it is garbled; this reason
|
|
65
|
+
// is reserved for a target that is well-formed and simply is not this host's controller.
|
|
66
|
+
if (target.origin !== request.allowedOrigin) {
|
|
67
|
+
return refuse('bulk-target-not-controller', `refusing to deliver to ${target.origin}: this host posts bulk content only to its own ` +
|
|
68
|
+
`controller at ${request.allowedOrigin}. The destination rides the wire, so a peer that ` +
|
|
69
|
+
`names another origin is asking for the transcript AND this host's durable credential`);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const stats = await stat(absolute.value);
|
|
73
|
+
const start = Math.min(Math.max(0, request.fromOffset), stats.size);
|
|
74
|
+
const byteCount = stats.size - start;
|
|
75
|
+
// Bounded to the bytes the header promised. `content-length` is declared from the stat above,
|
|
76
|
+
// but the stream was opened after it: reading a live transcript (the headline use case) lets
|
|
77
|
+
// the CLI append between the two, so an unbounded stream drains past the declared length and
|
|
78
|
+
// the delivery dies as `bulk-delivery-failed`, a name that blames the delivery for a file that
|
|
79
|
+
// simply moved. The end offset is inclusive, hence the -1.
|
|
80
|
+
//
|
|
81
|
+
// The zero-byte arm is not `end: start - 1`: `end` must be a non-negative integer, so an
|
|
82
|
+
// empty delivery at offset 0 would throw rather than send nothing. An empty stream is the
|
|
83
|
+
// honest encoding of "the header promised no bytes".
|
|
84
|
+
const stream = byteCount > 0
|
|
85
|
+
? createReadStream(absolute.value, { start, end: start + byteCount - 1 })
|
|
86
|
+
: Readable.from([]);
|
|
87
|
+
const response = await fetch(target, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'content-type': 'application/octet-stream',
|
|
91
|
+
'content-length': String(byteCount),
|
|
92
|
+
'x-delivery-id': request.deliveryId,
|
|
93
|
+
...request.headers,
|
|
94
|
+
},
|
|
95
|
+
body: Readable.toWeb(stream),
|
|
96
|
+
// Required by undici whenever the body is a stream: the request body is still being sent
|
|
97
|
+
// while the response is read. Without it the fetch rejects before a byte moves.
|
|
98
|
+
duplex: 'half',
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
return refuse('bulk-delivery-failed', `controller answered ${response.status} ${response.statusText} for delivery ${request.deliveryId}`);
|
|
102
|
+
}
|
|
103
|
+
return ok({
|
|
104
|
+
deliveryId: request.deliveryId,
|
|
105
|
+
byteCount,
|
|
106
|
+
sizeBytes: stats.size,
|
|
107
|
+
mtimeMs: Math.floor(stats.mtimeMs),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return refuse('bulk-delivery-failed', `delivery ${request.deliveryId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=bulk-post.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Result } from '../core/result.js';
|
|
2
|
+
/**
|
|
3
|
+
* The strict name allowlist — drive/path characters only. `'.'` and `'..'` PASS this regex, which
|
|
4
|
+
* is exactly why the dot-name reject below is its own layer rather than a tightening of this one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const NAME_ALLOWLIST: RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* The agent's home when nothing configures one: the folder the agent CLI keeps its state in, under
|
|
9
|
+
* the user's home directory. Null when there is no home to derive it from — a refusable state,
|
|
10
|
+
* never a fallback: inventing one would turn "this machine has no home directory" into a silent
|
|
11
|
+
* empty listing that reads as a fresh machine.
|
|
12
|
+
*/
|
|
13
|
+
export declare function defaultAgentHome(env?: NodeJS.ProcessEnv): string | null;
|
|
14
|
+
/** Where the agent CLI keeps transcripts under its home: derived, never configured on its own. */
|
|
15
|
+
export declare function transcriptsRootUnder(agentHome: string): string;
|
|
16
|
+
/** The transcripts root under the default agent home, or null when there is no home. */
|
|
17
|
+
export declare function claudeProjectsRoot(env?: NodeJS.ProcessEnv): string | null;
|
|
18
|
+
/** One transcript on disk. `mtimeMs` is a floored integer so equality against a stamp holds. */
|
|
19
|
+
export interface DiscoveredTranscript {
|
|
20
|
+
readonly projectSlug: string;
|
|
21
|
+
readonly sessionId: string;
|
|
22
|
+
readonly sizeBytes: number;
|
|
23
|
+
readonly mtimeMs: number;
|
|
24
|
+
/**
|
|
25
|
+
* The working directory the CLI recorded on its entries, read off the file's own head, never
|
|
26
|
+
* decoded from the slug (the flattening is not invertible). The resume handle's other half: the
|
|
27
|
+
* CLI keeps transcripts per cwd, so a resume that does not run there finds nothing. Null when the
|
|
28
|
+
* head carries none.
|
|
29
|
+
*/
|
|
30
|
+
readonly cwd: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface TranscriptPage {
|
|
33
|
+
readonly entries: readonly DiscoveredTranscript[];
|
|
34
|
+
readonly totalCount: number;
|
|
35
|
+
/** Ask again from here for the next page; null when this page ends the listing. */
|
|
36
|
+
readonly nextIndex: number | null;
|
|
37
|
+
}
|
|
38
|
+
/** What a tail probe answers. `absent` true means the transcript does not exist — a real negative. */
|
|
39
|
+
export interface TranscriptTailAnswer {
|
|
40
|
+
readonly absent: boolean;
|
|
41
|
+
readonly found: boolean;
|
|
42
|
+
readonly newOffset: number;
|
|
43
|
+
readonly sizeBytes: number | null;
|
|
44
|
+
readonly mtimeMs: number | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* THE JAIL. Resolve `{root}/{projectSlug}/{sessionId}.jsonl` with all three layers applied to both
|
|
48
|
+
* caller-supplied names. The one path-shaping function in this module — list, tail and the bulk
|
|
49
|
+
* resolver all come through here, so there is no second door to keep honest.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveTranscriptPath(root: string, projectSlug: string, sessionId: string): Result<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Enumerate every session transcript: direct-child `*.jsonl` per slug directory (nested files are
|
|
54
|
+
* the CLI's subagent transcripts and are not part of this listing). Newest first by mtime, paged.
|
|
55
|
+
* A missing root is a fresh machine and answers an empty listing, never an error; a file deleted
|
|
56
|
+
* between readdir and stat is skipped for the same reason.
|
|
57
|
+
*/
|
|
58
|
+
export declare function listTranscripts(root: string, page?: {
|
|
59
|
+
fromIndex?: number;
|
|
60
|
+
pageSize?: number;
|
|
61
|
+
}): Promise<TranscriptPage>;
|
|
62
|
+
/**
|
|
63
|
+
* Probe one transcript from `fromOffset` for a user entry matching `needle` (null = any user-text
|
|
64
|
+
* entry). Jail violations refuse; an absent file answers `{absent: true}`; a read that FAILS is a
|
|
65
|
+
* refusal (`transcript-read-failed`), never a false negative — to the asker "no new entry" and
|
|
66
|
+
* "could not look" must not be the same answer.
|
|
67
|
+
*/
|
|
68
|
+
export declare function tailTranscript(root: string, projectSlug: string, sessionId: string, probe?: {
|
|
69
|
+
fromOffset?: number;
|
|
70
|
+
needle?: string | null;
|
|
71
|
+
}): Promise<Result<TranscriptTailAnswer>>;
|
|
72
|
+
/**
|
|
73
|
+
* The bulk-lane resolver for transcript reads: `claude-transcript:{slug}/{sessionId}` to a jailed
|
|
74
|
+
* absolute path. A locator outside the namespace is refused as an invalid bulk target so an
|
|
75
|
+
* embedder composing several resolvers can tell "not mine" from "mine and malformed".
|
|
76
|
+
*/
|
|
77
|
+
export declare function claudeTranscriptResolver(root: string): (what: string, sessionKey: string) => Result<string>;
|
|
78
|
+
/**
|
|
79
|
+
* One line: does it count as a matching user entry? Unparseable or foreign lines are skipped,
|
|
80
|
+
* never thrown — a truncated LAST line is the normal state of a file being appended to. A
|
|
81
|
+
* tool-result-only user line has no text block and so never matches.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isMatchingUserEntry(line: string, needle: string | null): boolean;
|
|
84
|
+
//# sourceMappingURL=claude-transcripts.d.ts.map
|