@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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-Schema to Zod raw shape, because that is the shape the SDK's `tool()` actually takes.
|
|
3
|
+
*
|
|
4
|
+
* Why a conversion exists at all. `SdkMcpToolDefinition<Schema extends AnyZodRawShape>` is generic
|
|
5
|
+
* over a Zod raw shape — a plain record of Zod types, not a schema object and not JSON Schema. A
|
|
6
|
+
* design in which descriptors carry `inputSchema: JsonSchema` and the host does
|
|
7
|
+
* `validate(input, d.inputSchema)` does not compile against the shipped types. A controller in
|
|
8
|
+
* another language cannot express Zod shapes, so the conversion happens here and the descriptor
|
|
9
|
+
* contract stays JSON.
|
|
10
|
+
*
|
|
11
|
+
* An unconvertible construct is refused. Never widened, never `z.any()`, never skipped.
|
|
12
|
+
* This is the most consequential line in the directory. A converter that met something it did not
|
|
13
|
+
* recognise and fell back to a permissive schema would register the tool successfully and validate
|
|
14
|
+
* nothing — so "a malformed call is rejected before it reaches the controller" would be silently
|
|
15
|
+
* false for exactly the tools nobody looked at, while every test stayed green and the tool appeared
|
|
16
|
+
* to work. That is a false green in the highest-consequence direction: the failure is invisible
|
|
17
|
+
* precisely where the checking was supposed to be. Refusing happens at registration, before any
|
|
18
|
+
* session exists, and names the construct that could not be converted.
|
|
19
|
+
*
|
|
20
|
+
* The subset is deliberately small — what a controller emits for a tool's arguments. Growing it is
|
|
21
|
+
* a normal change; growing it by accident is what the refusal prevents.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { ok, refuse } from '../core/result.js';
|
|
25
|
+
import { SUPPORTED_SCHEMA_TYPES } from './descriptor.js';
|
|
26
|
+
/**
|
|
27
|
+
* Convert a tool's input schema into a raw shape.
|
|
28
|
+
*
|
|
29
|
+
* An object with no properties converts to an empty shape rather than being refused — a tool that
|
|
30
|
+
* takes no arguments is ordinary, and refusing it would force every caller to invent a parameter.
|
|
31
|
+
*/
|
|
32
|
+
export function toRawShape(schema, at = 'inputSchema') {
|
|
33
|
+
if (!isRecordLike(schema)) {
|
|
34
|
+
return refuse('mcp-schema-unsupported', `${at} is not an object`);
|
|
35
|
+
}
|
|
36
|
+
if (schema.type !== undefined && schema.type !== 'object') {
|
|
37
|
+
// A tool's arguments are always a named set. A top-level array or string has no property names
|
|
38
|
+
// to become a shape's keys, so there is nothing to convert it into.
|
|
39
|
+
return refuse('mcp-schema-unsupported', `${at} must be an object schema, not ${String(schema.type)} — a tool's arguments are always named`);
|
|
40
|
+
}
|
|
41
|
+
const properties = schema.properties ?? {};
|
|
42
|
+
if (!isRecordLike(properties)) {
|
|
43
|
+
return refuse('mcp-schema-unsupported', `${at}.properties is not an object`);
|
|
44
|
+
}
|
|
45
|
+
const required = new Set(schema.required ?? []);
|
|
46
|
+
for (const name of required) {
|
|
47
|
+
if (!(name in properties)) {
|
|
48
|
+
// A required property with no definition would silently vanish from the shape, so a call
|
|
49
|
+
// omitting it would pass — the schema says it is mandatory and nothing would enforce that.
|
|
50
|
+
return refuse('mcp-schema-unsupported', `${at}.required names "${name}", which has no entry in properties`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const shape = {};
|
|
54
|
+
for (const [name, node] of Object.entries(properties)) {
|
|
55
|
+
const converted = toZod(node, `${at}.properties.${name}`);
|
|
56
|
+
if (!converted.ok)
|
|
57
|
+
return refuse(converted.refusal.reason, converted.refusal.detail);
|
|
58
|
+
// Optional is the default, exactly as JSON Schema says: a property is required only when
|
|
59
|
+
// `required` names it. Getting this backwards would make every argument mandatory and every
|
|
60
|
+
// legitimate call fail.
|
|
61
|
+
shape[name] = required.has(name) ? converted.value : converted.value.optional();
|
|
62
|
+
}
|
|
63
|
+
return ok(shape);
|
|
64
|
+
}
|
|
65
|
+
/** Convert one node. Recursive for `array` and `object`. */
|
|
66
|
+
export function toZod(node, at) {
|
|
67
|
+
if (!isRecordLike(node)) {
|
|
68
|
+
return refuse('mcp-schema-unsupported', `${at} is not a schema object`);
|
|
69
|
+
}
|
|
70
|
+
const base = baseType(node, at);
|
|
71
|
+
if (!base.ok)
|
|
72
|
+
return base;
|
|
73
|
+
let schema = base.value;
|
|
74
|
+
if (node.nullable === true)
|
|
75
|
+
schema = schema.nullable();
|
|
76
|
+
if (typeof node.description === 'string' && node.description !== '') {
|
|
77
|
+
// Carried because the model reads it. A converted schema that dropped descriptions would produce
|
|
78
|
+
// tools whose arguments are undocumented to the only reader that matters.
|
|
79
|
+
schema = schema.describe(node.description);
|
|
80
|
+
}
|
|
81
|
+
return ok(schema);
|
|
82
|
+
}
|
|
83
|
+
function baseType(node, at) {
|
|
84
|
+
// `enum` wins over `type`: a closed set of literals is a stronger statement than the type of its
|
|
85
|
+
// members, and an emitter that sends both means the enum.
|
|
86
|
+
if (node.enum !== undefined)
|
|
87
|
+
return fromEnum(node.enum, at);
|
|
88
|
+
if (node.type === undefined) {
|
|
89
|
+
// A typeless node is refused, and this is the case a fallback would swallow. In
|
|
90
|
+
// JSON Schema, no `type` means "anything" — so converting it to a permissive schema is the
|
|
91
|
+
// literally correct reading and also the one that turns validation off for that property
|
|
92
|
+
// without saying so.
|
|
93
|
+
return refuse('mcp-schema-unsupported', `${at} declares no type, which would accept any value — state a type or an enum rather than validating nothing`);
|
|
94
|
+
}
|
|
95
|
+
switch (node.type) {
|
|
96
|
+
case 'string':
|
|
97
|
+
return ok(z.string());
|
|
98
|
+
case 'boolean':
|
|
99
|
+
return ok(z.boolean());
|
|
100
|
+
case 'number':
|
|
101
|
+
return ok(z.number());
|
|
102
|
+
case 'integer':
|
|
103
|
+
return ok(z.number().int());
|
|
104
|
+
case 'array':
|
|
105
|
+
return fromArray(node, at);
|
|
106
|
+
case 'object':
|
|
107
|
+
return fromObject(node, at);
|
|
108
|
+
default:
|
|
109
|
+
return refuse('mcp-schema-unsupported', `${at} has type "${String(node.type)}", which this host cannot convert — supported: ${SUPPORTED_SCHEMA_TYPES.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function fromArray(node, at) {
|
|
113
|
+
if (node.items === undefined) {
|
|
114
|
+
// An array with no `items` accepts a list of anything, which is the same hole as a typeless
|
|
115
|
+
// property one level down.
|
|
116
|
+
return refuse('mcp-schema-unsupported', `${at} is an array with no items schema, so its elements would not be validated at all`);
|
|
117
|
+
}
|
|
118
|
+
const items = toZod(node.items, `${at}.items`);
|
|
119
|
+
if (!items.ok)
|
|
120
|
+
return items;
|
|
121
|
+
return ok(z.array(items.value));
|
|
122
|
+
}
|
|
123
|
+
function fromObject(node, at) {
|
|
124
|
+
const properties = node.properties ?? {};
|
|
125
|
+
if (!isRecordLike(properties)) {
|
|
126
|
+
return refuse('mcp-schema-unsupported', `${at}.properties is not an object`);
|
|
127
|
+
}
|
|
128
|
+
if (Object.keys(properties).length === 0) {
|
|
129
|
+
return refuse('mcp-schema-unsupported', `${at} is an object with no properties, so any object would satisfy it`);
|
|
130
|
+
}
|
|
131
|
+
const required = new Set(node.required ?? []);
|
|
132
|
+
const shape = {};
|
|
133
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
134
|
+
const converted = toZod(child, `${at}.properties.${name}`);
|
|
135
|
+
if (!converted.ok)
|
|
136
|
+
return converted;
|
|
137
|
+
shape[name] = required.has(name) ? converted.value : converted.value.optional();
|
|
138
|
+
}
|
|
139
|
+
return ok(z.object(shape));
|
|
140
|
+
}
|
|
141
|
+
function fromEnum(values, at) {
|
|
142
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
143
|
+
return refuse('mcp-schema-unsupported', `${at}.enum is empty, so no value could satisfy it`);
|
|
144
|
+
}
|
|
145
|
+
const literals = [];
|
|
146
|
+
for (const value of values) {
|
|
147
|
+
if (typeof value !== 'string' &&
|
|
148
|
+
typeof value !== 'number' &&
|
|
149
|
+
typeof value !== 'boolean' &&
|
|
150
|
+
value !== null) {
|
|
151
|
+
return refuse('mcp-schema-unsupported', `${at}.enum carries a non-primitive value (${JSON.stringify(value)}); only strings, numbers, booleans and null convert`);
|
|
152
|
+
}
|
|
153
|
+
literals.push(value === null ? z.null() : z.literal(value));
|
|
154
|
+
}
|
|
155
|
+
const [first, second, ...rest] = literals;
|
|
156
|
+
if (first === undefined) {
|
|
157
|
+
return refuse('mcp-schema-unsupported', `${at}.enum is empty`);
|
|
158
|
+
}
|
|
159
|
+
// A one-value enum is a literal; a union needs at least two members.
|
|
160
|
+
if (second === undefined)
|
|
161
|
+
return ok(first);
|
|
162
|
+
return ok(z.union([first, second, ...rest]));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A plain object, checked at runtime without narrowing the compile-time type.
|
|
166
|
+
*
|
|
167
|
+
* It returns `boolean`, not a type predicate, on purpose. These values arrive typed as schema
|
|
168
|
+
* nodes but come off the wire, so the check has to run — and a predicate would re-narrow an already
|
|
169
|
+
* precise type down to `Record<string, unknown>`, turning every child node into `unknown` and
|
|
170
|
+
* forcing casts back to the type the caller already had.
|
|
171
|
+
*/
|
|
172
|
+
function isRecordLike(value) {
|
|
173
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Descriptors in, tool plans out — the whole registration path, with no SDK anywhere in it.
|
|
3
|
+
*
|
|
4
|
+
* Why this file stops short of the SDK. Only `src/host/` names `@anthropic-ai/claude-agent-sdk`,
|
|
5
|
+
* pinned by pins/sdk-confinement.test.ts, so that one directory answers "what can start a process
|
|
6
|
+
* here?". Building an in-process MCP server is not a spawn — but the rule is about the specifier,
|
|
7
|
+
* not about which of the package's functions happen to be dangerous, and weakening it for a safe
|
|
8
|
+
* call is how the boundary stops meaning anything. So the decisions live here and
|
|
9
|
+
* `host/mcp-server.ts` turns a plan into an actual server. Exactly the split the path jail uses
|
|
10
|
+
* (`gate/jail.ts` pure, `host/paths.ts` real) and the workspace providers use.
|
|
11
|
+
*
|
|
12
|
+
* The payoff is the same one it always is: the generic pin can fingerprint a plan and compare two
|
|
13
|
+
* registrations without an SDK object anywhere near the test.
|
|
14
|
+
*
|
|
15
|
+
* The host never learns what a tool means. Every tool takes one path: validate the descriptor,
|
|
16
|
+
* convert the schema it was handed, check a call's arguments against that schema, attach session
|
|
17
|
+
* identity, forward to the invoker, return the answer. There is no branch on a tool's name anywhere
|
|
18
|
+
* — pinned by pins/mcp-generic.test.ts, which plans the same descriptors under permuted names and
|
|
19
|
+
* requires the results to be identical.
|
|
20
|
+
*
|
|
21
|
+
* No `MCP_TIMEOUT` knob, deliberately. A 30-second connect window lost under a spawn storm, and a
|
|
22
|
+
* CLI that never reconnects a failed stdio server, are process-transport failures. An in-process
|
|
23
|
+
* server never connects, so it cannot lose a race and has nothing to reconnect. Porting the knob
|
|
24
|
+
* anyway would document a failure mode this design does not have.
|
|
25
|
+
*/
|
|
26
|
+
import type { JsonObject } from '../control/frames.js';
|
|
27
|
+
import type { Result } from '../core/result.js';
|
|
28
|
+
import type { ToolDescriptor } from './descriptor.js';
|
|
29
|
+
import type { ZodRawShape } from './schema.js';
|
|
30
|
+
/** Which session a call came from. Read at call time — see `identity` below. */
|
|
31
|
+
export interface ToolCallIdentity {
|
|
32
|
+
/**
|
|
33
|
+
* The agent's own session id, or null before it has reported one.
|
|
34
|
+
*
|
|
35
|
+
* Null is reachable and is not an error. An agent does not report itself until its first turn
|
|
36
|
+
* has been queued, so a server composed at session start genuinely has no id yet. An invoker
|
|
37
|
+
* receiving null knows the call arrived before the agent named itself, which is information —
|
|
38
|
+
* inventing a placeholder id would destroy it.
|
|
39
|
+
*/
|
|
40
|
+
readonly sessionId: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** One forwarded call, in the invoker's terms. */
|
|
43
|
+
export interface ToolCall {
|
|
44
|
+
readonly serverName: string;
|
|
45
|
+
readonly toolName: string;
|
|
46
|
+
/** Already validated against the tool's own schema. */
|
|
47
|
+
readonly arguments: JsonObject;
|
|
48
|
+
readonly identity: ToolCallIdentity;
|
|
49
|
+
}
|
|
50
|
+
/** What the invoker answers with. */
|
|
51
|
+
export interface ToolResponse {
|
|
52
|
+
readonly text: string;
|
|
53
|
+
/** Reported to the model as an error result. Defaults to false. */
|
|
54
|
+
readonly isError?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** How a validated call reaches the embedder's invoker, the only thing that knows what a tool does. */
|
|
57
|
+
export type ToolInvoker = (call: ToolCall) => Promise<ToolResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* A tool result in the shape MCP expects. Structural, so this file needs no SDK type.
|
|
60
|
+
*
|
|
61
|
+
* `content` is mutable, against this package's habit, because the SDK's own result type declares a
|
|
62
|
+
* mutable array and a `readonly` one is not assignable to it. Stated rather than left as a puzzle:
|
|
63
|
+
* the alternative is a cast at the seam, which would hide any future divergence in that shape.
|
|
64
|
+
*/
|
|
65
|
+
export interface ToolResult {
|
|
66
|
+
readonly content: {
|
|
67
|
+
type: 'text';
|
|
68
|
+
text: string;
|
|
69
|
+
}[];
|
|
70
|
+
readonly isError?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One tool, ready to be handed to the SDK.
|
|
74
|
+
*
|
|
75
|
+
* Everything about a tool that this package decides. `host/mcp-server.ts` adds nothing but the
|
|
76
|
+
* SDK call — which is what makes a plan a complete and checkable statement of the registration.
|
|
77
|
+
*/
|
|
78
|
+
export interface ToolPlan {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly description: string;
|
|
81
|
+
readonly shape: ZodRawShape;
|
|
82
|
+
readonly handler: (args: unknown) => Promise<ToolResult>;
|
|
83
|
+
}
|
|
84
|
+
export interface McpServerOptions {
|
|
85
|
+
/** The server's name. Tools reach the model as `mcp__{name}__{tool}`. */
|
|
86
|
+
readonly name: string;
|
|
87
|
+
readonly version?: string;
|
|
88
|
+
readonly descriptors: readonly ToolDescriptor[];
|
|
89
|
+
readonly invoke: ToolInvoker;
|
|
90
|
+
/**
|
|
91
|
+
* Session identity, read at call time rather than captured at composition time.
|
|
92
|
+
*
|
|
93
|
+
* A function, not a value, and the reason is a measured SDK property. The server has to exist
|
|
94
|
+
* before `query()` is called, and the agent does not report its session id until its first turn
|
|
95
|
+
* has been queued — so a value captured here would be null forever, on every call, for the whole
|
|
96
|
+
* session. Reading it lazily is the only shape that can ever carry a real id.
|
|
97
|
+
*/
|
|
98
|
+
readonly identity: () => ToolCallIdentity;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Turn descriptors into plans.
|
|
102
|
+
*
|
|
103
|
+
* Refuses rather than throwing: this runs before any session exists, and an embedder that supplied an
|
|
104
|
+
* unusable descriptor needs to be told which one and why.
|
|
105
|
+
*/
|
|
106
|
+
export declare function planTools(options: McpServerOptions): Result<ToolPlan[]>;
|
|
107
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ok, refuse } from '../core/result.js';
|
|
2
|
+
import { isUsableToolName } from './descriptor.js';
|
|
3
|
+
import { toRawShape } from './schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* Turn descriptors into plans.
|
|
6
|
+
*
|
|
7
|
+
* Refuses rather than throwing: this runs before any session exists, and an embedder that supplied an
|
|
8
|
+
* unusable descriptor needs to be told which one and why.
|
|
9
|
+
*/
|
|
10
|
+
export function planTools(options) {
|
|
11
|
+
if (!isUsableToolName(options.name)) {
|
|
12
|
+
return refuse('mcp-descriptor-invalid', `"${options.name}" is not a usable server name — letters, digits, hyphen and single underscores only`);
|
|
13
|
+
}
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const plans = [];
|
|
16
|
+
for (const descriptor of options.descriptors) {
|
|
17
|
+
const invalid = rejectDescriptor(descriptor, seen);
|
|
18
|
+
if (invalid !== null)
|
|
19
|
+
return invalid;
|
|
20
|
+
seen.add(descriptor.name);
|
|
21
|
+
const shape = toRawShape(descriptor.inputSchema, `${descriptor.name}.inputSchema`);
|
|
22
|
+
if (!shape.ok)
|
|
23
|
+
return refuse(shape.refusal.reason, shape.refusal.detail);
|
|
24
|
+
// The handler closes over the tool's name as data. It reads nothing else about the tool and
|
|
25
|
+
// branches on nothing — every tool in every server runs this identical body.
|
|
26
|
+
const toolName = descriptor.name;
|
|
27
|
+
const handler = async (args) => {
|
|
28
|
+
const response = await options.invoke({
|
|
29
|
+
serverName: options.name,
|
|
30
|
+
toolName,
|
|
31
|
+
arguments: asJsonObject(args),
|
|
32
|
+
identity: options.identity(),
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: response.text }],
|
|
36
|
+
...(response.isError === true ? { isError: true } : {}),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
plans.push({ name: descriptor.name, description: descriptor.description, shape: shape.value, handler });
|
|
40
|
+
}
|
|
41
|
+
return ok(plans);
|
|
42
|
+
}
|
|
43
|
+
function rejectDescriptor(descriptor, seen) {
|
|
44
|
+
if (typeof descriptor?.name !== 'string' || !isUsableToolName(descriptor.name)) {
|
|
45
|
+
return refuse('mcp-descriptor-invalid', `"${String(descriptor?.name)}" is not a usable tool name — letters, digits, hyphen and single underscores only`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof descriptor.description !== 'string' || descriptor.description.trim() === '') {
|
|
48
|
+
// The description is the only thing that tells the model what the tool is for. A tool without
|
|
49
|
+
// one is registered, callable, and never called — which looks like a broken tool, not a bad
|
|
50
|
+
// descriptor.
|
|
51
|
+
return refuse('mcp-descriptor-invalid', `tool "${descriptor.name}" has no description`);
|
|
52
|
+
}
|
|
53
|
+
if (seen.has(descriptor.name)) {
|
|
54
|
+
// Two tools with one name: the second silently shadows the first, so an embedder would see
|
|
55
|
+
// calls it expected to reach one tool arrive at another.
|
|
56
|
+
return refuse('mcp-descriptor-invalid', `tool "${descriptor.name}" is described twice in one server`);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/** The SDK hands the handler its parsed arguments; this narrows them for the wire. */
|
|
61
|
+
function asJsonObject(args) {
|
|
62
|
+
if (typeof args !== 'object' || args === null || Array.isArray(args))
|
|
63
|
+
return {};
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One stored line, and the codec that turns a transcript into lines and back.
|
|
3
|
+
*
|
|
4
|
+
* Entries are pass-through blobs and this file keeps them that way. The concrete entry shape is
|
|
5
|
+
* the on-disk transcript format — a large union the CLI owns and the SDK deliberately does not
|
|
6
|
+
* export, exposing only a structural supertype: a string `type`, usually a `uuid`, usually a
|
|
7
|
+
* `timestamp`, and opaque JSON for the rest. So nothing here parses an entry into a richer model.
|
|
8
|
+
* Round-tripping through JSON is the only invariant the adapter contract requires, and it is
|
|
9
|
+
* therefore the one this file pins.
|
|
10
|
+
*
|
|
11
|
+
* A line that will not round-trip is named, never skipped. Skipping is how a transcript loses
|
|
12
|
+
* entries nobody counted — the reader sees a shorter list and no reason to doubt it, which is the
|
|
13
|
+
* exact silent-loss shape a receipt read must not sit on top of.
|
|
14
|
+
*/
|
|
15
|
+
import type { Result } from '../core/result.js';
|
|
16
|
+
/** One transcript line. Structurally the adapter contract's entry, under this package's own name. */
|
|
17
|
+
export interface TranscriptEntry {
|
|
18
|
+
readonly type: string;
|
|
19
|
+
/** Most entries carry one. Titles, tags and mode markers do not — see `mirror.ts` on dedup. */
|
|
20
|
+
readonly uuid?: string;
|
|
21
|
+
readonly timestamp?: string;
|
|
22
|
+
readonly [key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The discriminators that mark an entry as produced by compaction rather than by a turn.
|
|
26
|
+
*
|
|
27
|
+
* This list is why a receipt cannot report a false positive, and it is the defence whose failure
|
|
28
|
+
* direction is opposite to the other two. Compaction replaces earlier turns with a summary, and a
|
|
29
|
+
* summary is fed back into the conversation as ordinary-looking content. A receipt asking only "is
|
|
30
|
+
* there a user entry after my anchor?" would match the summary and report a delivery that never
|
|
31
|
+
* happened — the controller then believes text landed that the agent never saw.
|
|
32
|
+
*
|
|
33
|
+
* Kept as data, because the entry union is CLI-internal and this is a structural read of it. If
|
|
34
|
+
* the format grows another marker, one constant changes and every reader that matters is already
|
|
35
|
+
* looking at it. `subtype` is the SDK's own word: `SDKCompactBoundaryMessage` carries
|
|
36
|
+
* `subtype: 'compact_boundary'` alongside its `compact_metadata`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const COMPACTION_SUBTYPES: readonly string[];
|
|
39
|
+
/** Entry `type` values that exist only because a compaction ran. */
|
|
40
|
+
export declare const COMPACTION_TYPES: readonly string[];
|
|
41
|
+
/** The key the boundary's own metadata rides on, named once. */
|
|
42
|
+
export declare const COMPACT_METADATA_KEY = "compact_metadata";
|
|
43
|
+
/**
|
|
44
|
+
* Whether this entry is the compaction boundary itself — the marker that says a compaction ran here.
|
|
45
|
+
*
|
|
46
|
+
* Three independent tells, because the boundary is the one entry a receipt read must never miss:
|
|
47
|
+
* the SDK's own subtype, the entry type, and the presence of the metadata block the boundary is the
|
|
48
|
+
* only thing that carries.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isCompactBoundary(entry: TranscriptEntry): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether this entry exists because a compaction ran — the boundary, or the summary it produced.
|
|
53
|
+
*
|
|
54
|
+
* Broader than `isCompactBoundary` on purpose: the boundary is what a reader counts, and this is
|
|
55
|
+
* what a reader must not mistake for a turn.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCompactionProduced(entry: TranscriptEntry): boolean;
|
|
58
|
+
/** Whether this entry is a user turn — the thing a delivery receipt is looking for. */
|
|
59
|
+
export declare function isUserEntry(entry: TranscriptEntry): boolean;
|
|
60
|
+
/** One entry as one JSONL line. No trailing newline — the writer joins. */
|
|
61
|
+
export declare function encodeEntry(entry: TranscriptEntry): string;
|
|
62
|
+
/** One JSONL line back to an entry, or a named refusal. */
|
|
63
|
+
export declare function decodeEntry(line: string): Result<TranscriptEntry>;
|
|
64
|
+
/** A whole transcript as JSONL text. */
|
|
65
|
+
export declare function encodeTranscript(entries: readonly TranscriptEntry[]): string;
|
|
66
|
+
/**
|
|
67
|
+
* JSONL text back to entries.
|
|
68
|
+
*
|
|
69
|
+
* Blank lines are skipped because a trailing newline is ordinary and produces one; a line with
|
|
70
|
+
* content that will not decode is refused, and the refusal names which line so the file can be
|
|
71
|
+
* looked at. Those are different situations and only one of them is a problem.
|
|
72
|
+
*/
|
|
73
|
+
export declare function decodeTranscript(text: string): Result<TranscriptEntry[]>;
|
|
74
|
+
//# sourceMappingURL=entry.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ok, refuse } from '../core/result.js';
|
|
2
|
+
/**
|
|
3
|
+
* The discriminators that mark an entry as produced by compaction rather than by a turn.
|
|
4
|
+
*
|
|
5
|
+
* This list is why a receipt cannot report a false positive, and it is the defence whose failure
|
|
6
|
+
* direction is opposite to the other two. Compaction replaces earlier turns with a summary, and a
|
|
7
|
+
* summary is fed back into the conversation as ordinary-looking content. A receipt asking only "is
|
|
8
|
+
* there a user entry after my anchor?" would match the summary and report a delivery that never
|
|
9
|
+
* happened — the controller then believes text landed that the agent never saw.
|
|
10
|
+
*
|
|
11
|
+
* Kept as data, because the entry union is CLI-internal and this is a structural read of it. If
|
|
12
|
+
* the format grows another marker, one constant changes and every reader that matters is already
|
|
13
|
+
* looking at it. `subtype` is the SDK's own word: `SDKCompactBoundaryMessage` carries
|
|
14
|
+
* `subtype: 'compact_boundary'` alongside its `compact_metadata`.
|
|
15
|
+
*/
|
|
16
|
+
export const COMPACTION_SUBTYPES = ['compact_boundary'];
|
|
17
|
+
/** Entry `type` values that exist only because a compaction ran. */
|
|
18
|
+
export const COMPACTION_TYPES = ['summary', 'compact_boundary'];
|
|
19
|
+
/** The key the boundary's own metadata rides on, named once. */
|
|
20
|
+
export const COMPACT_METADATA_KEY = 'compact_metadata';
|
|
21
|
+
/**
|
|
22
|
+
* Whether this entry is the compaction boundary itself — the marker that says a compaction ran here.
|
|
23
|
+
*
|
|
24
|
+
* Three independent tells, because the boundary is the one entry a receipt read must never miss:
|
|
25
|
+
* the SDK's own subtype, the entry type, and the presence of the metadata block the boundary is the
|
|
26
|
+
* only thing that carries.
|
|
27
|
+
*/
|
|
28
|
+
export function isCompactBoundary(entry) {
|
|
29
|
+
const subtype = entry['subtype'];
|
|
30
|
+
if (typeof subtype === 'string' && COMPACTION_SUBTYPES.includes(subtype))
|
|
31
|
+
return true;
|
|
32
|
+
if (entry.type === 'compact_boundary')
|
|
33
|
+
return true;
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(entry, COMPACT_METADATA_KEY);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether this entry exists because a compaction ran — the boundary, or the summary it produced.
|
|
38
|
+
*
|
|
39
|
+
* Broader than `isCompactBoundary` on purpose: the boundary is what a reader counts, and this is
|
|
40
|
+
* what a reader must not mistake for a turn.
|
|
41
|
+
*/
|
|
42
|
+
export function isCompactionProduced(entry) {
|
|
43
|
+
if (isCompactBoundary(entry))
|
|
44
|
+
return true;
|
|
45
|
+
if (COMPACTION_TYPES.includes(entry.type))
|
|
46
|
+
return true;
|
|
47
|
+
return entry['isCompactSummary'] === true;
|
|
48
|
+
}
|
|
49
|
+
/** Whether this entry is a user turn — the thing a delivery receipt is looking for. */
|
|
50
|
+
export function isUserEntry(entry) {
|
|
51
|
+
return entry.type === 'user';
|
|
52
|
+
}
|
|
53
|
+
/** One entry as one JSONL line. No trailing newline — the writer joins. */
|
|
54
|
+
export function encodeEntry(entry) {
|
|
55
|
+
return JSON.stringify(entry);
|
|
56
|
+
}
|
|
57
|
+
/** One JSONL line back to an entry, or a named refusal. */
|
|
58
|
+
export function decodeEntry(line) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(line);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return refuse('transcript-entry-malformed', `line is not JSON: ${preview(line)}`);
|
|
65
|
+
}
|
|
66
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
67
|
+
return refuse('transcript-entry-malformed', `line is not a JSON object: ${preview(line)}`);
|
|
68
|
+
}
|
|
69
|
+
const record = parsed;
|
|
70
|
+
if (typeof record['type'] !== 'string') {
|
|
71
|
+
return refuse('transcript-entry-malformed', `entry has no string "type" discriminant: ${preview(line)}`);
|
|
72
|
+
}
|
|
73
|
+
return ok(record);
|
|
74
|
+
}
|
|
75
|
+
/** A whole transcript as JSONL text. */
|
|
76
|
+
export function encodeTranscript(entries) {
|
|
77
|
+
return entries.map(encodeEntry).join('\n');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* JSONL text back to entries.
|
|
81
|
+
*
|
|
82
|
+
* Blank lines are skipped because a trailing newline is ordinary and produces one; a line with
|
|
83
|
+
* content that will not decode is refused, and the refusal names which line so the file can be
|
|
84
|
+
* looked at. Those are different situations and only one of them is a problem.
|
|
85
|
+
*/
|
|
86
|
+
export function decodeTranscript(text) {
|
|
87
|
+
const entries = [];
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
90
|
+
const line = (lines[index] ?? '').trim();
|
|
91
|
+
if (line.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
const decoded = decodeEntry(line);
|
|
94
|
+
if (!decoded.ok) {
|
|
95
|
+
return refuse(decoded.refusal.reason, `line ${index + 1}: ${decoded.refusal.detail}`);
|
|
96
|
+
}
|
|
97
|
+
entries.push(decoded.value);
|
|
98
|
+
}
|
|
99
|
+
return ok(entries);
|
|
100
|
+
}
|
|
101
|
+
function preview(line) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
return trimmed.length <= 80 ? trimmed : `${trimmed.slice(0, 80)}…`;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript durability, the receipt read path, and the durable transition log.
|
|
3
|
+
*
|
|
4
|
+
* The one rule a consumer must not get wrong: a receipt, an audit, or any "what actually
|
|
5
|
+
* happened?" question is answered from raw stored entries — `store.load(key)` or the local JSONL —
|
|
6
|
+
* and never from the SDK's conversation reader, which returns the post-compaction view. "What would
|
|
7
|
+
* the agent see?" is the other question and the other read. `receipt.ts` carries the reasoning.
|
|
8
|
+
*/
|
|
9
|
+
export type { TranscriptKey } from './key.js';
|
|
10
|
+
export { sameTranscript, transcriptKey, transcriptToken } from './key.js';
|
|
11
|
+
export type { TranscriptEntry } from './entry.js';
|
|
12
|
+
export { COMPACTION_SUBTYPES, COMPACTION_TYPES, COMPACT_METADATA_KEY, decodeEntry, decodeTranscript, encodeEntry, encodeTranscript, isCompactBoundary, isCompactionProduced, isUserEntry, } from './entry.js';
|
|
13
|
+
export type { ReceiptOutcome, ReceiptQuery } from './receipt.js';
|
|
14
|
+
export { baselineAnchor, compactionCount, resolveReceipt } from './receipt.js';
|
|
15
|
+
export { ABANDONED_ENTRY_TYPE, TRANSITION_ENTRY_TYPE, abandonmentsIn, decodeTransition, encodeTransition, markAbandoned, transitionUuid, transitionsIn, } from './transition-log.js';
|
|
16
|
+
export type { RetentionPlan, RetentionPolicy } from './retention.js';
|
|
17
|
+
export { entryTimeMs, markOutlivesSubject, planRetention } from './retention.js';
|
|
18
|
+
export type { DedupedBatch, MirrorDrop, MirrorFailureKind } from './mirror.js';
|
|
19
|
+
export { MIRROR_ATTEMPTS, dedupeBatch, describeMirrorDrop, uuidsIn } from './mirror.js';
|
|
20
|
+
export type { JsonlStoreOptions, StoreEffects, StoredSession, TranscriptStore } from './store.js';
|
|
21
|
+
export { createJsonlStore } from './store.js';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { sameTranscript, transcriptKey, transcriptToken } from './key.js';
|
|
2
|
+
export { COMPACTION_SUBTYPES, COMPACTION_TYPES, COMPACT_METADATA_KEY, decodeEntry, decodeTranscript, encodeEntry, encodeTranscript, isCompactBoundary, isCompactionProduced, isUserEntry, } from './entry.js';
|
|
3
|
+
export { baselineAnchor, compactionCount, resolveReceipt } from './receipt.js';
|
|
4
|
+
export { ABANDONED_ENTRY_TYPE, TRANSITION_ENTRY_TYPE, abandonmentsIn, decodeTransition, encodeTransition, markAbandoned, transitionUuid, transitionsIn, } from './transition-log.js';
|
|
5
|
+
export { entryTimeMs, markOutlivesSubject, planRetention } from './retention.js';
|
|
6
|
+
export { MIRROR_ATTEMPTS, dedupeBatch, describeMirrorDrop, uuidsIn } from './mirror.js';
|
|
7
|
+
export { createJsonlStore } from './store.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transcript — the address of one session's stored entries.
|
|
3
|
+
*
|
|
4
|
+
* This is this package's own type, not the SDK's, and the duplication is deliberate. The store
|
|
5
|
+
* adapter's key is an SDK type, and `src/host/` is the only directory allowed to name the SDK
|
|
6
|
+
* (pinned by pins/sdk-confinement.test.ts). Everything worth testing about a key — what is refused,
|
|
7
|
+
* how a subagent's transcript is addressed, how one becomes a storage path — is pure logic, so it
|
|
8
|
+
* lives here and `host/session-store.ts` bridges the two shapes. The same split the workspace
|
|
9
|
+
* providers and the path jail already use.
|
|
10
|
+
*
|
|
11
|
+
* The three fields are structurally the adapter contract's, and that is the point: bridging is a
|
|
12
|
+
* field-for-field copy with nothing to get wrong, and a shape change on either side is a compile
|
|
13
|
+
* error in one file rather than a silent mismatch.
|
|
14
|
+
*/
|
|
15
|
+
import type { Result } from '../core/result.js';
|
|
16
|
+
/**
|
|
17
|
+
* One transcript, addressed.
|
|
18
|
+
*
|
|
19
|
+
* `subpath` absent and `subpath` empty are different things, and conflating them reads the wrong
|
|
20
|
+
* transcript. Absent means the session's main transcript; a subagent's is addressed by a subpath
|
|
21
|
+
* that mirrors the on-disk directory. An empty string is neither — the adapter contract calls it
|
|
22
|
+
* invalid and says to omit the field — so an empty one is refused rather than normalised away. A
|
|
23
|
+
* normaliser here would answer a question about a subagent with the main transcript's contents,
|
|
24
|
+
* which is a wrong answer wearing a right answer's shape.
|
|
25
|
+
*/
|
|
26
|
+
export interface TranscriptKey {
|
|
27
|
+
/** The caller's scope — a tenant, a project. Never derived here; the embedder decides. */
|
|
28
|
+
readonly projectKey: string;
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
/** Absent = the main transcript. Present = a subagent's. Opaque: never parsed. */
|
|
31
|
+
readonly subpath?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Build a key, or say which part was unusable. */
|
|
34
|
+
export declare function transcriptKey(projectKey: string, sessionId: string, subpath?: string): Result<TranscriptKey>;
|
|
35
|
+
/** Whether two keys address the same transcript. */
|
|
36
|
+
export declare function sameTranscript(left: TranscriptKey, right: TranscriptKey): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* A stable, comparable string for one key. For map keys and log lines — never parsed back.
|
|
39
|
+
*
|
|
40
|
+
* It is not a filesystem path and must not be used as one. A projectKey is caller-supplied and can
|
|
41
|
+
* carry anything, including separators; turning that into a path is how a key climbs out of its
|
|
42
|
+
* directory. `host/transcript-fs.ts` derives real paths through the same jail every other host-side
|
|
43
|
+
* path goes through, and this exists only so two keys can be compared or logged as one token.
|
|
44
|
+
*/
|
|
45
|
+
export declare function transcriptToken(key: TranscriptKey): string;
|
|
46
|
+
//# sourceMappingURL=key.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ok, refuse } from '../core/result.js';
|
|
2
|
+
/** Build a key, or say which part was unusable. */
|
|
3
|
+
export function transcriptKey(projectKey, sessionId, subpath) {
|
|
4
|
+
if (projectKey.length === 0) {
|
|
5
|
+
return refuse('transcript-key-invalid', 'projectKey is empty');
|
|
6
|
+
}
|
|
7
|
+
if (sessionId.length === 0) {
|
|
8
|
+
return refuse('transcript-key-invalid', 'sessionId is empty');
|
|
9
|
+
}
|
|
10
|
+
if (subpath !== undefined && subpath.length === 0) {
|
|
11
|
+
return refuse('transcript-key-invalid', 'subpath is present and empty — omit the field for the main transcript');
|
|
12
|
+
}
|
|
13
|
+
return ok(subpath === undefined ? { projectKey, sessionId } : { projectKey, sessionId, subpath });
|
|
14
|
+
}
|
|
15
|
+
/** Whether two keys address the same transcript. */
|
|
16
|
+
export function sameTranscript(left, right) {
|
|
17
|
+
return (left.projectKey === right.projectKey &&
|
|
18
|
+
left.sessionId === right.sessionId &&
|
|
19
|
+
(left.subpath ?? null) === (right.subpath ?? null));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A stable, comparable string for one key. For map keys and log lines — never parsed back.
|
|
23
|
+
*
|
|
24
|
+
* It is not a filesystem path and must not be used as one. A projectKey is caller-supplied and can
|
|
25
|
+
* carry anything, including separators; turning that into a path is how a key climbs out of its
|
|
26
|
+
* directory. `host/transcript-fs.ts` derives real paths through the same jail every other host-side
|
|
27
|
+
* path goes through, and this exists only so two keys can be compared or logged as one token.
|
|
28
|
+
*/
|
|
29
|
+
export function transcriptToken(key) {
|
|
30
|
+
const main = `${encodeURIComponent(key.projectKey)}/${encodeURIComponent(key.sessionId)}`;
|
|
31
|
+
return key.subpath === undefined ? main : `${main}/${encodeURIComponent(key.subpath)}`;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=key.js.map
|