@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (RFC 7636) and the callback's `state` — the two random values that make an authorization-code
|
|
3
|
+
* exchange safe to run on a machine somebody else is also running code on.
|
|
4
|
+
*
|
|
5
|
+
* They defend different things and neither substitutes for the other. This is the sentence worth
|
|
6
|
+
* reading twice, because "it does PKCE" is routinely taken to mean the whole callback is covered:
|
|
7
|
+
*
|
|
8
|
+
* - The code verifier proves that whoever redeems the authorization code is the same party
|
|
9
|
+
* that requested it. It protects the code exchange.
|
|
10
|
+
* - The state proves that the callback this host received is the answer to the request this
|
|
11
|
+
* host made. It protects the callback itself.
|
|
12
|
+
*
|
|
13
|
+
* Why that matters acutely here, rather than as boilerplate. This host's whole premise is that
|
|
14
|
+
* the agent runs as the same OS user as the host — it is why an 0600 token file is not a boundary
|
|
15
|
+
* and why the gate has a credential-path denial. That same fact means any local process can
|
|
16
|
+
* connect to the loopback listener this flow opens. Without `state`, such a process could hand this
|
|
17
|
+
* host an authorization code of its own obtaining and have the host redeem it and cache the
|
|
18
|
+
* resulting token. `state` is what makes that a named refusal instead of a silent substitution.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
21
|
+
import { ok, refuse } from '../core/result.js';
|
|
22
|
+
/**
|
|
23
|
+
* The only challenge method this host will use.
|
|
24
|
+
*
|
|
25
|
+
* RFC 7636 also defines `plain`, where the challenge is the verifier. This host refuses it rather
|
|
26
|
+
* than merely not offering it — an unused branch is one a later reader restores on a compatibility
|
|
27
|
+
* report, and `plain` gives away the whole property to anyone who can observe the authorization
|
|
28
|
+
* request.
|
|
29
|
+
*/
|
|
30
|
+
export const CODE_CHALLENGE_METHOD = 'S256';
|
|
31
|
+
/** 32 bytes, which is 43 base64url characters — the RFC's floor is 43 and its ceiling is 128. */
|
|
32
|
+
const ENTROPY_BYTES = 32;
|
|
33
|
+
/**
|
|
34
|
+
* base64url, per RFC 4648 §5 — no padding, URL-safe alphabet.
|
|
35
|
+
*
|
|
36
|
+
* Hand-rolled from base64 rather than reached for by name: `Buffer`'s `base64url` encoding is
|
|
37
|
+
* equivalent, but the transform is one line and stating it keeps this readable next to the RFC.
|
|
38
|
+
*/
|
|
39
|
+
function base64Url(bytes) {
|
|
40
|
+
return bytes.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A cryptographically random, URL-safe string.
|
|
44
|
+
*
|
|
45
|
+
* `randomBytes`, never `Math.random`. `Math.random` is seeded, predictable and explicitly not for
|
|
46
|
+
* this; a predictable verifier or state defeats the property entirely while looking identical in
|
|
47
|
+
* every test.
|
|
48
|
+
*/
|
|
49
|
+
export function randomUrlSafe(bytes = ENTROPY_BYTES) {
|
|
50
|
+
return base64Url(randomBytes(bytes));
|
|
51
|
+
}
|
|
52
|
+
/** A fresh verifier and its S256 challenge. */
|
|
53
|
+
export function createPkce() {
|
|
54
|
+
const verifier = randomUrlSafe();
|
|
55
|
+
return { verifier, challenge: challengeFor(verifier), method: CODE_CHALLENGE_METHOD };
|
|
56
|
+
}
|
|
57
|
+
/** The S256 challenge for a verifier: base64url(SHA-256(ascii(verifier))). */
|
|
58
|
+
export function challengeFor(verifier) {
|
|
59
|
+
return base64Url(createHash('sha256').update(verifier, 'ascii').digest());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The `state` value for one authorization request.
|
|
63
|
+
*
|
|
64
|
+
* Same entropy source as the verifier and deliberately a separate value — reusing the verifier as
|
|
65
|
+
* state would put it in a URL the browser and the provider both log, and the verifier is the one
|
|
66
|
+
* that must never leave this process until the token exchange.
|
|
67
|
+
*/
|
|
68
|
+
export function createState() {
|
|
69
|
+
return randomUrlSafe();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Refuse a challenge method this host will not use.
|
|
73
|
+
*
|
|
74
|
+
* Exported so the refusal is reachable and testable, rather than being an `if` nobody can drive.
|
|
75
|
+
*/
|
|
76
|
+
export function requireS256(method) {
|
|
77
|
+
if (method !== CODE_CHALLENGE_METHOD) {
|
|
78
|
+
return refuse('pkce-method-unsupported', `code challenge method ${method} is refused; this host uses ${CODE_CHALLENGE_METHOD} only, and "plain" would put the verifier itself in the authorization request`);
|
|
79
|
+
}
|
|
80
|
+
return ok(CODE_CHALLENGE_METHOD);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compare two `state` values without leaking their contents through timing.
|
|
84
|
+
*
|
|
85
|
+
* The length check is separate and deliberate: `timingSafeEqual` throws on unequal lengths, so
|
|
86
|
+
* comparing lengths first is required rather than an optimisation. It leaks the length and nothing
|
|
87
|
+
* else, which is the standard accepted residual.
|
|
88
|
+
*/
|
|
89
|
+
export function stateMatches(expected, received) {
|
|
90
|
+
const a = Buffer.from(expected, 'utf8');
|
|
91
|
+
const b = Buffer.from(received, 'utf8');
|
|
92
|
+
return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is persisted, and the port that persists it.
|
|
3
|
+
*
|
|
4
|
+
* The shape and its validation live here, not with the filesystem, and the dependency direction
|
|
5
|
+
* is the reason: `host/` may import this, and this may never import `host/`. Reading a cache is two
|
|
6
|
+
* jobs — getting bytes off a disk, and deciding whether what they say is usable — and only the
|
|
7
|
+
* first needs a machine. Keeping the second here means every malformed-cache branch is drivable
|
|
8
|
+
* without writing a file.
|
|
9
|
+
*/
|
|
10
|
+
import type { Result } from '../core/result.js';
|
|
11
|
+
import type { AuthProtocol } from './device-code.js';
|
|
12
|
+
import type { TokenSet } from './token.js';
|
|
13
|
+
/**
|
|
14
|
+
* The persisted record.
|
|
15
|
+
*
|
|
16
|
+
* `authority` and `clientId` ride along so a cache minted against one provider is never presented
|
|
17
|
+
* to another — a re-pointed host discards rather than replays, which matters because the token
|
|
18
|
+
* would otherwise be sent to an audience it was not issued for. `protocol` exists for the
|
|
19
|
+
* protocol-mismatch guard: material minted by one flow is discarded rather than refreshed when the
|
|
20
|
+
* host is configured for the other.
|
|
21
|
+
*/
|
|
22
|
+
export interface CachedTokens {
|
|
23
|
+
readonly tokens: TokenSet;
|
|
24
|
+
readonly protocol: AuthProtocol;
|
|
25
|
+
readonly authority: string;
|
|
26
|
+
readonly clientId: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The port the credential holds.
|
|
30
|
+
*
|
|
31
|
+
* An interface rather than the concrete file cache so the credential's whole decision surface —
|
|
32
|
+
* expired, re-pointed, wrong protocol, corrupt — is testable with an in-memory double, and so an
|
|
33
|
+
* embedder with its own secret storage can supply one without this package growing an opinion
|
|
34
|
+
* about keychains.
|
|
35
|
+
*/
|
|
36
|
+
export interface TokenStore {
|
|
37
|
+
read(): Result<CachedTokens>;
|
|
38
|
+
write(cached: CachedTokens): Result<unknown>;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/** Validate a parsed cache into the shape the rest of the host relies on. */
|
|
42
|
+
export declare function readCachedTokens(parsed: unknown): Result<CachedTokens>;
|
|
43
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ok, refuse } from '../core/result.js';
|
|
2
|
+
/** Validate a parsed cache into the shape the rest of the host relies on. */
|
|
3
|
+
export function readCachedTokens(parsed) {
|
|
4
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
5
|
+
return refuse('credential-cache-unreadable', 'the token cache is not a JSON object');
|
|
6
|
+
}
|
|
7
|
+
const record = parsed;
|
|
8
|
+
const tokens = record['tokens'];
|
|
9
|
+
const protocol = record['protocol'];
|
|
10
|
+
const authority = record['authority'];
|
|
11
|
+
const clientId = record['clientId'];
|
|
12
|
+
if (typeof tokens !== 'object' || tokens === null) {
|
|
13
|
+
return refuse('credential-cache-unreadable', 'the token cache carries no tokens object');
|
|
14
|
+
}
|
|
15
|
+
const tokenRecord = tokens;
|
|
16
|
+
const accessToken = tokenRecord['accessToken'];
|
|
17
|
+
const expiresAt = tokenRecord['expiresAt'];
|
|
18
|
+
if (typeof accessToken !== 'string' || accessToken === '' || typeof expiresAt !== 'number') {
|
|
19
|
+
return refuse('credential-cache-unreadable', 'the cached token has no accessToken or no numeric expiresAt');
|
|
20
|
+
}
|
|
21
|
+
if (protocol !== 'loopback' && protocol !== 'device-code') {
|
|
22
|
+
return refuse('credential-cache-unreadable', 'the token cache does not say which flow minted it, so it cannot be safely refreshed');
|
|
23
|
+
}
|
|
24
|
+
if (typeof authority !== 'string' || authority === '' || typeof clientId !== 'string' || clientId === '') {
|
|
25
|
+
return refuse('credential-cache-unreadable', 'the token cache does not record the authority and client it was minted for');
|
|
26
|
+
}
|
|
27
|
+
const refreshToken = tokenRecord['refreshToken'];
|
|
28
|
+
const tokenType = tokenRecord['tokenType'];
|
|
29
|
+
const scope = tokenRecord['scope'];
|
|
30
|
+
return ok({
|
|
31
|
+
tokens: {
|
|
32
|
+
accessToken,
|
|
33
|
+
refreshToken: typeof refreshToken === 'string' && refreshToken !== '' ? refreshToken : null,
|
|
34
|
+
expiresAt,
|
|
35
|
+
tokenType: typeof tokenType === 'string' && tokenType !== '' ? tokenType : 'Bearer',
|
|
36
|
+
scope: typeof scope === 'string' ? scope : null,
|
|
37
|
+
},
|
|
38
|
+
protocol,
|
|
39
|
+
authority,
|
|
40
|
+
clientId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token exchange — building the requests, reading the responses, and deciding when what the
|
|
3
|
+
* host holds is still usable. Pure: no network here, so every branch including the ugly ones is
|
|
4
|
+
* drivable.
|
|
5
|
+
*
|
|
6
|
+
* The redaction in this file is load-bearing, not hygiene. Mapping a provider's error response
|
|
7
|
+
* into a readable message is the single most likely place a token ends up in a log, because the
|
|
8
|
+
* natural implementation — echo the body so the operator can see what happened — is also the one
|
|
9
|
+
* that writes credential material to disk. Everything a provider says passes through
|
|
10
|
+
* `redactSecrets` before it becomes a refusal `detail`, and `token.test.ts` drives a failure path
|
|
11
|
+
* carrying a token and asserts the token appears nowhere in the output.
|
|
12
|
+
*/
|
|
13
|
+
import type { Result } from '../core/result.js';
|
|
14
|
+
import type { IdentityConfig } from './config.js';
|
|
15
|
+
/** What this host holds after a successful exchange. */
|
|
16
|
+
export interface TokenSet {
|
|
17
|
+
readonly accessToken: string;
|
|
18
|
+
/** Null when the provider issued none — then expiry means an interactive sign-in. */
|
|
19
|
+
readonly refreshToken: string | null;
|
|
20
|
+
/** Epoch milliseconds. */
|
|
21
|
+
readonly expiresAt: number;
|
|
22
|
+
readonly tokenType: string;
|
|
23
|
+
readonly scope: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How long before expiry a token is treated as already expired.
|
|
27
|
+
*
|
|
28
|
+
* A token that expires in four seconds is not usable for a request that takes five. The skew also
|
|
29
|
+
* covers clock drift between this host and the provider, which is why it is generous.
|
|
30
|
+
*/
|
|
31
|
+
export declare const EXPIRY_SKEW_MS = 60000;
|
|
32
|
+
/**
|
|
33
|
+
* Collect every credential-shaped value in a parsed body.
|
|
34
|
+
*
|
|
35
|
+
* Recursive because providers nest error payloads, and a token one level down is exactly as
|
|
36
|
+
* damaging as one at the top.
|
|
37
|
+
*/
|
|
38
|
+
export declare function secretsIn(body: unknown): string[];
|
|
39
|
+
/** Replace every known secret with a marker. Longest first, so a prefix cannot leave a tail behind. */
|
|
40
|
+
export declare function redactSecrets(text: string, secrets: readonly string[]): string;
|
|
41
|
+
/** The authorization-code redemption body. */
|
|
42
|
+
export declare function codeExchangeBody(config: IdentityConfig, code: string, codeVerifier: string, redirectUri: string): string;
|
|
43
|
+
/** The refresh body. */
|
|
44
|
+
export declare function refreshBody(config: IdentityConfig, refreshToken: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Turn a token-endpoint response into a `TokenSet`, or into the right kind of refusal.
|
|
47
|
+
*
|
|
48
|
+
* Two failure reasons, and they must not be collapsed — the same discipline the permission gate
|
|
49
|
+
* uses. A non-2xx is the provider refusing or an outage: the fix is a credential, a tenant policy,
|
|
50
|
+
* or a retry. A 2xx whose body cannot be used is version skew or a misconfigured endpoint: the fix
|
|
51
|
+
* is code or configuration. One is the provider's problem and one is this host's.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readTokenResponse(status: number, body: unknown, nowMs: number): Result<TokenSet>;
|
|
54
|
+
/** Is this token still usable, allowing for the skew? */
|
|
55
|
+
export declare function isFresh(tokens: TokenSet, nowMs: number): boolean;
|
|
56
|
+
/** The header a fresh token is presented as. */
|
|
57
|
+
export declare function authorizationValue(tokens: TokenSet): string;
|
|
58
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ok, refuse } from '../core/result.js';
|
|
2
|
+
/**
|
|
3
|
+
* How long before expiry a token is treated as already expired.
|
|
4
|
+
*
|
|
5
|
+
* A token that expires in four seconds is not usable for a request that takes five. The skew also
|
|
6
|
+
* covers clock drift between this host and the provider, which is why it is generous.
|
|
7
|
+
*/
|
|
8
|
+
export const EXPIRY_SKEW_MS = 60_000;
|
|
9
|
+
/** The field names whose values must never reach a log, at any nesting depth. */
|
|
10
|
+
const SECRET_FIELDS = ['access_token', 'refresh_token', 'id_token', 'code', 'code_verifier', 'client_secret'];
|
|
11
|
+
/**
|
|
12
|
+
* Collect every credential-shaped value in a parsed body.
|
|
13
|
+
*
|
|
14
|
+
* Recursive because providers nest error payloads, and a token one level down is exactly as
|
|
15
|
+
* damaging as one at the top.
|
|
16
|
+
*/
|
|
17
|
+
export function secretsIn(body) {
|
|
18
|
+
const found = [];
|
|
19
|
+
const visit = (node, depth) => {
|
|
20
|
+
if (depth > 8 || typeof node !== 'object' || node === null)
|
|
21
|
+
return;
|
|
22
|
+
if (Array.isArray(node)) {
|
|
23
|
+
for (const item of node)
|
|
24
|
+
visit(item, depth + 1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(node)) {
|
|
28
|
+
if (SECRET_FIELDS.includes(key) && typeof value === 'string' && value !== '')
|
|
29
|
+
found.push(value);
|
|
30
|
+
else
|
|
31
|
+
visit(value, depth + 1);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
visit(body, 0);
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
/** Replace every known secret with a marker. Longest first, so a prefix cannot leave a tail behind. */
|
|
38
|
+
export function redactSecrets(text, secrets) {
|
|
39
|
+
let out = text;
|
|
40
|
+
for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
|
|
41
|
+
if (secret === '')
|
|
42
|
+
continue;
|
|
43
|
+
out = out.split(secret).join('[redacted]');
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Bound a provider-supplied string before it becomes a message. */
|
|
48
|
+
function bounded(value, max = 200) {
|
|
49
|
+
return value.length <= max ? value : `${value.slice(0, max)}… (${value.length} chars)`;
|
|
50
|
+
}
|
|
51
|
+
function form(pairs) {
|
|
52
|
+
const body = new URLSearchParams();
|
|
53
|
+
for (const [key, value] of Object.entries(pairs))
|
|
54
|
+
body.set(key, value);
|
|
55
|
+
return body.toString();
|
|
56
|
+
}
|
|
57
|
+
/** The authorization-code redemption body. */
|
|
58
|
+
export function codeExchangeBody(config, code, codeVerifier, redirectUri) {
|
|
59
|
+
return form({
|
|
60
|
+
grant_type: 'authorization_code',
|
|
61
|
+
client_id: config.clientId,
|
|
62
|
+
code,
|
|
63
|
+
redirect_uri: redirectUri,
|
|
64
|
+
code_verifier: codeVerifier,
|
|
65
|
+
scope: config.scopes.join(' '),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The refresh body. */
|
|
69
|
+
export function refreshBody(config, refreshToken) {
|
|
70
|
+
return form({
|
|
71
|
+
grant_type: 'refresh_token',
|
|
72
|
+
client_id: config.clientId,
|
|
73
|
+
refresh_token: refreshToken,
|
|
74
|
+
scope: config.scopes.join(' '),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turn a token-endpoint response into a `TokenSet`, or into the right kind of refusal.
|
|
79
|
+
*
|
|
80
|
+
* Two failure reasons, and they must not be collapsed — the same discipline the permission gate
|
|
81
|
+
* uses. A non-2xx is the provider refusing or an outage: the fix is a credential, a tenant policy,
|
|
82
|
+
* or a retry. A 2xx whose body cannot be used is version skew or a misconfigured endpoint: the fix
|
|
83
|
+
* is code or configuration. One is the provider's problem and one is this host's.
|
|
84
|
+
*/
|
|
85
|
+
export function readTokenResponse(status, body, nowMs) {
|
|
86
|
+
const secrets = secretsIn(body);
|
|
87
|
+
if (status < 200 || status >= 300) {
|
|
88
|
+
const record = typeof body === 'object' && body !== null ? body : {};
|
|
89
|
+
const error = typeof record['error'] === 'string' ? record['error'] : `HTTP ${status}`;
|
|
90
|
+
const description = typeof record['error_description'] === 'string' ? record['error_description'] : null;
|
|
91
|
+
// Only the two named fields are quoted — the body is never dumped. That is the first of two
|
|
92
|
+
// defences here, and it is the one that is easy to lose: "echo what the provider said so the
|
|
93
|
+
// operator can see it" is the natural implementation and it writes tokens to a log.
|
|
94
|
+
const message = description === null ? error : `${error}: ${description}`;
|
|
95
|
+
// `invalid_grant` is a different answer from every other non-2xx, and conflating them hides a
|
|
96
|
+
// dead grant behind retries. RFC 6749 section 5.2 defines it as the grant being expired, revoked,
|
|
97
|
+
// or issued to another client - none of which the next attempt improves. Everything else here is
|
|
98
|
+
// something a retry may well survive: a gateway hiccup, a throttle, a provider blip.
|
|
99
|
+
//
|
|
100
|
+
// This reader serves both grant exchanges, and the conclusion holds for each. On a refresh the
|
|
101
|
+
// refresh token has lapsed; on the initial code exchange the authorization code has expired or
|
|
102
|
+
// been replayed. Either way the material presented is spent and the next attempt with the same
|
|
103
|
+
// material fails identically - what differs is only which sign-in a person has to redo.
|
|
104
|
+
//
|
|
105
|
+
// The name is the RFC's, not a provider's. Providers append their own diagnostic codes to
|
|
106
|
+
// `error_description` and no one provider's is canonical; branching on the standard name is
|
|
107
|
+
// what keeps the rule true for a host pointed somewhere else.
|
|
108
|
+
if (error === 'invalid_grant') {
|
|
109
|
+
return refuse('token-grant-rejected', redactSecrets(bounded(`${message} - this grant is no longer valid; a new sign-in is required`), secrets));
|
|
110
|
+
}
|
|
111
|
+
return refuse('token-request-failed', redactSecrets(bounded(message), secrets));
|
|
112
|
+
}
|
|
113
|
+
if (typeof body !== 'object' || body === null) {
|
|
114
|
+
return refuse('token-response-invalid', 'the token endpoint returned 2xx with a body that is not a JSON object');
|
|
115
|
+
}
|
|
116
|
+
const record = body;
|
|
117
|
+
const accessToken = record['access_token'];
|
|
118
|
+
if (typeof accessToken !== 'string' || accessToken === '') {
|
|
119
|
+
return refuse('token-response-invalid', 'the token endpoint returned 2xx with no access_token');
|
|
120
|
+
}
|
|
121
|
+
// `expires_in` is only recommended by RFC 6749, and this host refuses without it anyway. An
|
|
122
|
+
// unattended host that does not know when its token dies cannot decide when to refresh — it would
|
|
123
|
+
// either refresh on every single call or discover expiry as a 401 in the middle of doing
|
|
124
|
+
// something. Neither is acceptable, and guessing a lifetime would be inventing a fact.
|
|
125
|
+
const expiresIn = record['expires_in'];
|
|
126
|
+
const seconds = typeof expiresIn === 'number' ? expiresIn : Number(expiresIn);
|
|
127
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
128
|
+
return refuse('token-response-invalid', 'the token endpoint returned 2xx with no usable expires_in; an unattended host cannot decide when to refresh a token whose lifetime it does not know');
|
|
129
|
+
}
|
|
130
|
+
const refreshToken = record['refresh_token'];
|
|
131
|
+
const scope = record['scope'];
|
|
132
|
+
const tokenType = record['token_type'];
|
|
133
|
+
return ok({
|
|
134
|
+
accessToken,
|
|
135
|
+
refreshToken: typeof refreshToken === 'string' && refreshToken !== '' ? refreshToken : null,
|
|
136
|
+
expiresAt: nowMs + seconds * 1000,
|
|
137
|
+
tokenType: typeof tokenType === 'string' && tokenType !== '' ? tokenType : 'Bearer',
|
|
138
|
+
scope: typeof scope === 'string' ? scope : null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Is this token still usable, allowing for the skew? */
|
|
142
|
+
export function isFresh(tokens, nowMs) {
|
|
143
|
+
return tokens.expiresAt - EXPIRY_SKEW_MS > nowMs;
|
|
144
|
+
}
|
|
145
|
+
/** The header a fresh token is presented as. */
|
|
146
|
+
export function authorizationValue(tokens) {
|
|
147
|
+
return `${tokens.tokenType} ${tokens.accessToken}`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=token.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of the package.
|
|
3
|
+
*
|
|
4
|
+
* Everything a consumer may depend on is named here or on the `./protocol` subpath. A module not
|
|
5
|
+
* re-exported from one of the two is internal, whatever its file path suggests.
|
|
6
|
+
*/
|
|
7
|
+
export * from './protocol.js';
|
|
8
|
+
export type { LinkHandlers, LinkOptions } from './control/link.js';
|
|
9
|
+
export { ControllerLink } from './control/link.js';
|
|
10
|
+
export type { ForwardSessionOptions, FrameSink } from './control/stream.js';
|
|
11
|
+
export { forwardSession } from './control/stream.js';
|
|
12
|
+
export type { RoutingRow, StreamLane } from './control/stream-routing.js';
|
|
13
|
+
export { MESSAGE_ROUTING, discriminatorsOn, laneFor } from './control/stream-routing.js';
|
|
14
|
+
export type { LinkCause, LinkState, LinkTransition } from './control/link-state.js';
|
|
15
|
+
export { LINK_CAUSES, LinkStateMachine } from './control/link-state.js';
|
|
16
|
+
export type { BackoffOptions } from './control/backoff.js';
|
|
17
|
+
export { DEFAULT_BACKOFF, nextDelayMs } from './control/backoff.js';
|
|
18
|
+
export type { QueueStats } from './control/queue.js';
|
|
19
|
+
export { BoundedFrameQueue } from './control/queue.js';
|
|
20
|
+
export type { Authorization, ControllerCredential } from './control/credential.js';
|
|
21
|
+
export { UnconfiguredCredential } from './control/credential.js';
|
|
22
|
+
export { HOST_NOUNS, SDK_NOUNS, isAbsolutePath, isContainedBy, isDeclaredNoun, isOk, nounOf, normalizePath, ok, refuse, refusal, requireAbsolute, systemClock, systemTicker, valueOr, } from './core/index.js';
|
|
23
|
+
export type { Clock, HostNoun, SdkNoun, Ticker } from './core/index.js';
|
|
24
|
+
export * from './state/index.js';
|
|
25
|
+
export * from './sessions/index.js';
|
|
26
|
+
export type { BulkPostReceipt, BulkPostRequest, MachineFacts } from './host/index.js';
|
|
27
|
+
export { credentialPaths, nodePathResolver, periscopeCredentialDir, postBulk, readMachineFacts, tokenCachePath, } from './host/index.js';
|
|
28
|
+
export type { BulkResolver, ComposeSessionOptions, ComposedSession, GateTimings, HostEvent, PeriscopeHostOptions, } from './host/index.js';
|
|
29
|
+
export { PeriscopeHost, composeSession } from './host/index.js';
|
|
30
|
+
export * from './identity/index.js';
|
|
31
|
+
export type { DeviceCodeInstruction, LoopbackListener, SignInDeps, TokenCacheWrite } from './host/index.js';
|
|
32
|
+
export { DEFAULT_SIGN_IN_TIMEOUT_MS, FileTokenCache, LOOPBACK_HOST, openLoopbackListener, probeModeEnforcement, protocolFor, refresherFor, resolveEndpoints, signIn, signInWithDeviceCode, } from './host/index.js';
|
|
33
|
+
export type { HookFailureListener, ObservationHookOptions } from './host/hooks.js';
|
|
34
|
+
export { mergeHooks, observationHooks, wiredHookEvents } from './host/hooks.js';
|
|
35
|
+
export { readWhere } from './host/git-facts.js';
|
|
36
|
+
export * from './gate/index.js';
|
|
37
|
+
export * from './workspace/index.js';
|
|
38
|
+
export { nodeCommandEffects, nodeWorkspaceEffects } from './host/workspace-fs.js';
|
|
39
|
+
export * from './mcp/index.js';
|
|
40
|
+
export { createToolServer } from './host/mcp-server.js';
|
|
41
|
+
export * from './persistence/index.js';
|
|
42
|
+
export { asSessionStore, nodeStoreEffects, readMirrorDrop, toSessionKey, toTranscriptKey, } from './host/index.js';
|
|
43
|
+
export * from './telemetry/index.js';
|
|
44
|
+
export { readRateLimit, readTaskSpend, readTurnSpend } from './host/index.js';
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of the package.
|
|
3
|
+
*
|
|
4
|
+
* Everything a consumer may depend on is named here or on the `./protocol` subpath. A module not
|
|
5
|
+
* re-exported from one of the two is internal, whatever its file path suggests.
|
|
6
|
+
*/
|
|
7
|
+
// The wire contract, re-exported so `@naswerks/periscope` alone is enough for a host-side consumer. A
|
|
8
|
+
// controller that wants only the wire should import `@naswerks/periscope/protocol` instead — that path
|
|
9
|
+
// cannot reach the privileged module.
|
|
10
|
+
export * from './protocol.js';
|
|
11
|
+
export { ControllerLink } from './control/link.js';
|
|
12
|
+
export { forwardSession } from './control/stream.js';
|
|
13
|
+
export { MESSAGE_ROUTING, discriminatorsOn, laneFor } from './control/stream-routing.js';
|
|
14
|
+
export { LINK_CAUSES, LinkStateMachine } from './control/link-state.js';
|
|
15
|
+
export { DEFAULT_BACKOFF, nextDelayMs } from './control/backoff.js';
|
|
16
|
+
export { BoundedFrameQueue } from './control/queue.js';
|
|
17
|
+
export { UnconfiguredCredential } from './control/credential.js';
|
|
18
|
+
// The pure core.
|
|
19
|
+
export { HOST_NOUNS, SDK_NOUNS, isAbsolutePath, isContainedBy, isDeclaredNoun, isOk, nounOf, normalizePath, ok, refuse, refusal, requireAbsolute, systemClock, systemTicker, valueOr, } from './core/index.js';
|
|
20
|
+
// The declared state model, its machine, its store and the coverage table.
|
|
21
|
+
export * from './state/index.js';
|
|
22
|
+
// The agent session lifecycle and the registry of live sessions.
|
|
23
|
+
export * from './sessions/index.js';
|
|
24
|
+
export { credentialPaths, nodePathResolver, periscopeCredentialDir, postBulk, readMachineFacts, tokenCachePath, } from './host/index.js';
|
|
25
|
+
export { PeriscopeHost, composeSession } from './host/index.js';
|
|
26
|
+
// Identity — the pure flows, plus the two impure halves that live in `host/` by the same rule as
|
|
27
|
+
// everything else that touches the machine.
|
|
28
|
+
export * from './identity/index.js';
|
|
29
|
+
export { DEFAULT_SIGN_IN_TIMEOUT_MS, FileTokenCache, LOOPBACK_HOST, openLoopbackListener, probeModeEnforcement, protocolFor, refresherFor, resolveEndpoints, signIn, signInWithDeviceCode, } from './host/index.js';
|
|
30
|
+
export { mergeHooks, observationHooks, wiredHookEvents } from './host/hooks.js';
|
|
31
|
+
export { readWhere } from './host/git-facts.js';
|
|
32
|
+
// The permission gate. Registered after observationHooks — see permissionHooks' own note on why the
|
|
33
|
+
// order is load-bearing.
|
|
34
|
+
export * from './gate/index.js';
|
|
35
|
+
// Workspace provisioning — the directory a session runs in. The two providers ship; the interesting
|
|
36
|
+
// policies (a shared directory per task, a branch naming rule, a cleanup schedule) are the
|
|
37
|
+
// embedder's and are written against `WorkspaceProvider` rather than added here.
|
|
38
|
+
export * from './workspace/index.js';
|
|
39
|
+
export { nodeCommandEffects, nodeWorkspaceEffects } from './host/workspace-fs.js';
|
|
40
|
+
// The generic tool mechanism: descriptors in, an in-process MCP server out. The host validates a
|
|
41
|
+
// call against the schema it was handed, attaches session identity, forwards, and returns — it never
|
|
42
|
+
// learns what any tool means. `createToolServer` sits in `host/` because it names the SDK.
|
|
43
|
+
export * from './mcp/index.js';
|
|
44
|
+
export { createToolServer } from './host/mcp-server.js';
|
|
45
|
+
// Durability. The pure half — the store contract, the receipt read path, the transition log's
|
|
46
|
+
// encoding and the retention policy — plus the two impure halves in `host/`: the local JSONL
|
|
47
|
+
// effects, and the bridge that presents a store to the SDK.
|
|
48
|
+
export * from './persistence/index.js';
|
|
49
|
+
export { asSessionStore, nodeStoreEffects, readMirrorDrop, toSessionKey, toTranscriptKey, } from './host/index.js';
|
|
50
|
+
// Cost, usage and rate limits — read from the agent's own result, never computed from a price table.
|
|
51
|
+
export * from './telemetry/index.js';
|
|
52
|
+
export { readRateLimit, readTaskSpend, readTurnSpend } from './host/index.js';
|
|
53
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The descriptor contract — what an embedder supplies so this host can register an in-process tool.
|
|
3
|
+
*
|
|
4
|
+
* The descriptor is deliberately language-neutral. An embedder composing this host from another
|
|
5
|
+
* language, or a controller whose descriptors an embedder forwards,
|
|
6
|
+
* cannot express a TypeScript schema library's types, and asking it to would put a TypeScript
|
|
7
|
+
* dependency on the wire — which is precisely the leak this package is built to avoid. So a
|
|
8
|
+
* descriptor is JSON: a name, a description, and a JSON-Schema object. The host converts it
|
|
9
|
+
* (schema.ts) into what the SDK needs.
|
|
10
|
+
*
|
|
11
|
+
* The host never learns what a tool means. It validates the descriptor's shape, converts the
|
|
12
|
+
* schema it was handed, checks a call's arguments against that schema, attaches session identity,
|
|
13
|
+
* forwards, and returns the answer. There is no branch anywhere on a tool's name — pinned by
|
|
14
|
+
* pins/mcp-generic.test.ts, which registers the same descriptors under permuted names and requires
|
|
15
|
+
* the results to be identical.
|
|
16
|
+
*
|
|
17
|
+
* The subset is declared, not inferred. Only the constructs below are convertible. A descriptor
|
|
18
|
+
* carrying anything else is refused at registration rather than converted to something permissive —
|
|
19
|
+
* see schema.ts, where that rule is the most consequential line in this directory.
|
|
20
|
+
*/
|
|
21
|
+
import type { JsonValue } from '../control/frames.js';
|
|
22
|
+
/** The JSON-Schema types this host converts. Anything else is refused by name. */
|
|
23
|
+
export declare const SUPPORTED_SCHEMA_TYPES: readonly ["string", "number", "integer", "boolean", "array", "object"];
|
|
24
|
+
export type SupportedSchemaType = (typeof SUPPORTED_SCHEMA_TYPES)[number];
|
|
25
|
+
/**
|
|
26
|
+
* One node of a tool's input schema.
|
|
27
|
+
*
|
|
28
|
+
* Everything is optional because this is what ARRIVES — an untrusted object off the wire, not a
|
|
29
|
+
* shape this package constructs. Validation happens in schema.ts and reports what was wrong.
|
|
30
|
+
*/
|
|
31
|
+
export interface JsonSchemaNode {
|
|
32
|
+
readonly type?: string;
|
|
33
|
+
readonly description?: string;
|
|
34
|
+
/** A closed set of literal values. Takes precedence over `type`. */
|
|
35
|
+
readonly enum?: readonly JsonValue[];
|
|
36
|
+
/** For `array`. */
|
|
37
|
+
readonly items?: JsonSchemaNode;
|
|
38
|
+
/** For `object`. */
|
|
39
|
+
readonly properties?: Readonly<Record<string, JsonSchemaNode>>;
|
|
40
|
+
readonly required?: readonly string[];
|
|
41
|
+
/** JSON-Schema draft-4 style, and the spelling most emitters produce. */
|
|
42
|
+
readonly nullable?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** A tool's input schema. Always an object at the top level — `tool()` takes a shape, not a type. */
|
|
45
|
+
export interface JsonSchemaObject extends JsonSchemaNode {
|
|
46
|
+
readonly type?: 'object';
|
|
47
|
+
readonly properties?: Readonly<Record<string, JsonSchemaNode>>;
|
|
48
|
+
readonly required?: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
/** One tool, as the embedder describes it. */
|
|
51
|
+
export interface ToolDescriptor {
|
|
52
|
+
/** What the agent calls. Reaches the model as `mcp__{server}__{name}`. */
|
|
53
|
+
readonly name: string;
|
|
54
|
+
/** What the tool does, in the model's terms. The only thing that makes it usable. */
|
|
55
|
+
readonly description: string;
|
|
56
|
+
readonly inputSchema: JsonSchemaObject;
|
|
57
|
+
}
|
|
58
|
+
export declare function isUsableToolName(name: string): boolean;
|
|
59
|
+
//# sourceMappingURL=descriptor.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The JSON-Schema types this host converts. Anything else is refused by name. */
|
|
2
|
+
export const SUPPORTED_SCHEMA_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object'];
|
|
3
|
+
/**
|
|
4
|
+
* A tool name both the MCP protocol and the agent CLI accept.
|
|
5
|
+
*
|
|
6
|
+
* Conservative on purpose: the name is concatenated into `mcp__{server}__{name}`, so a name carrying
|
|
7
|
+
* the separator would produce a tool the agent addresses ambiguously — and the failure would look
|
|
8
|
+
* like the wrong tool being called rather than like a bad name.
|
|
9
|
+
*/
|
|
10
|
+
const NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
11
|
+
export function isUsableToolName(name) {
|
|
12
|
+
return NAME_PATTERN.test(name) && !name.includes('__');
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=descriptor.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process MCP servers and the tool descriptors that build them.
|
|
3
|
+
*
|
|
4
|
+
* `createToolServer` is not here. It calls the SDK, so it lives in `host/mcp-server.ts` under the
|
|
5
|
+
* rule that one directory names the SDK — see that file. Everything in this directory is the
|
|
6
|
+
* decisions: the descriptor contract, the schema conversion, and the plan a server is built from.
|
|
7
|
+
*/
|
|
8
|
+
export type { JsonSchemaNode, JsonSchemaObject, SupportedSchemaType, ToolDescriptor } from './descriptor.js';
|
|
9
|
+
export { SUPPORTED_SCHEMA_TYPES, isUsableToolName } from './descriptor.js';
|
|
10
|
+
export type { ZodRawShape } from './schema.js';
|
|
11
|
+
export { toRawShape, toZod } from './schema.js';
|
|
12
|
+
export type { McpServerOptions, ToolCall, ToolCallIdentity, ToolInvoker, ToolPlan, ToolResponse, ToolResult, } from './server.js';
|
|
13
|
+
export { planTools } from './server.js';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
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 type { Result } from '../core/result.js';
|
|
25
|
+
import type { JsonSchemaNode, JsonSchemaObject } from './descriptor.js';
|
|
26
|
+
/** What `tool()` takes: a record of Zod types, one per top-level property. */
|
|
27
|
+
export type ZodRawShape = Record<string, z.ZodType>;
|
|
28
|
+
/**
|
|
29
|
+
* Convert a tool's input schema into a raw shape.
|
|
30
|
+
*
|
|
31
|
+
* An object with no properties converts to an empty shape rather than being refused — a tool that
|
|
32
|
+
* takes no arguments is ordinary, and refusing it would force every caller to invent a parameter.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toRawShape(schema: JsonSchemaObject, at?: string): Result<ZodRawShape>;
|
|
35
|
+
/** Convert one node. Recursive for `array` and `object`. */
|
|
36
|
+
export declare function toZod(node: JsonSchemaNode, at: string): Result<z.ZodType>;
|
|
37
|
+
//# sourceMappingURL=schema.d.ts.map
|