@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which worktree and which branch a directory sits in: the "where" half of every transition.
|
|
3
|
+
*
|
|
4
|
+
* Why this is read at all: a trace whose location is a bare path answers "where" with something
|
|
5
|
+
* a reader still has to go and look up. Worktree and branch are what makes a transition legible on
|
|
6
|
+
* its own, which is the whole bar the state model is held to.
|
|
7
|
+
*
|
|
8
|
+
* It reads; it never provisions. No `git` subprocess, no writes, no repository creation, just
|
|
9
|
+
* two small file reads. Creating and managing worktrees is a different job and stays one.
|
|
10
|
+
*
|
|
11
|
+
* `.git` is often a file, not a directory, and that is the trap this file exists around.
|
|
12
|
+
* In a linked worktree, `.git` is a text file holding `gitdir: <path>` and pointing at a directory
|
|
13
|
+
* under the main repository's `.git/worktrees/`. A walk-up that looks for a `.git` directory finds
|
|
14
|
+
* nothing there and reports "not a repository", which is wrong, silently, in exactly the
|
|
15
|
+
* environment agents are most often given.
|
|
16
|
+
*
|
|
17
|
+
* Every failure is a named null, never an exception and never an empty string. Three ordinary
|
|
18
|
+
* situations produce no branch: the directory is not in a repository at all (normal — a plain
|
|
19
|
+
* working directory is a legitimate place to run), HEAD is detached (there IS no branch name), or
|
|
20
|
+
* HEAD is unreadable. An empty string would make all three indistinguishable from each other and
|
|
21
|
+
* from a branch literally named "".
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
24
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
25
|
+
/** How far up to walk. A repository root further than this from cwd is not a case worth serving. */
|
|
26
|
+
const MAX_DEPTH = 64;
|
|
27
|
+
/**
|
|
28
|
+
* The `where` for a directory. Never throws — the caller stamps this onto every transition, so a
|
|
29
|
+
* throw here would take out the state record along with the fact it was decorating.
|
|
30
|
+
*/
|
|
31
|
+
export function readWhere(cwd) {
|
|
32
|
+
const located = locateGit(cwd);
|
|
33
|
+
if (located === null) {
|
|
34
|
+
return { cwd, worktree: null, branch: null, unknownReason: 'not inside a git repository' };
|
|
35
|
+
}
|
|
36
|
+
const head = readHead(located.gitDir);
|
|
37
|
+
return {
|
|
38
|
+
cwd,
|
|
39
|
+
worktree: located.worktree,
|
|
40
|
+
branch: head.branch,
|
|
41
|
+
unknownReason: head.reason,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Walk up from `start` for a `.git` entry, accepting both the directory and the pointer-file form. */
|
|
45
|
+
function locateGit(start) {
|
|
46
|
+
let current = resolve(start);
|
|
47
|
+
for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
|
|
48
|
+
const marker = join(current, '.git');
|
|
49
|
+
const kind = entryKind(marker);
|
|
50
|
+
if (kind === 'dir')
|
|
51
|
+
return { worktree: current, gitDir: marker };
|
|
52
|
+
if (kind === 'file') {
|
|
53
|
+
const pointed = readGitDirPointer(marker, current);
|
|
54
|
+
// A `.git` file that does not parse is still a `.git` file: this IS the worktree root, and
|
|
55
|
+
// saying so with an unreadable HEAD beats walking past it and reporting "not a repository".
|
|
56
|
+
return { worktree: current, gitDir: pointed ?? marker };
|
|
57
|
+
}
|
|
58
|
+
const parent = dirname(current);
|
|
59
|
+
if (parent === current)
|
|
60
|
+
return null;
|
|
61
|
+
current = parent;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function entryKind(path) {
|
|
66
|
+
try {
|
|
67
|
+
const stats = statSync(path);
|
|
68
|
+
return stats.isDirectory() ? 'dir' : 'file';
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return 'absent';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** `gitdir: <path>` out of a linked worktree's `.git` file. Relative paths resolve against it. */
|
|
75
|
+
function readGitDirPointer(marker, worktree) {
|
|
76
|
+
try {
|
|
77
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(marker, 'utf8'));
|
|
78
|
+
const target = pointer?.[1]?.trim();
|
|
79
|
+
if (target === undefined || target.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
return isAbsolute(target) ? target : resolve(worktree, target);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The branch, or which of the ordinary situations means there is not one. */
|
|
88
|
+
function readHead(gitDir) {
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
raw = readFileSync(join(gitDir, 'HEAD'), 'utf8').trim();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { branch: null, reason: 'HEAD could not be read' };
|
|
95
|
+
}
|
|
96
|
+
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(raw);
|
|
97
|
+
if (ref?.[1] !== undefined)
|
|
98
|
+
return { branch: ref[1].trim(), reason: null };
|
|
99
|
+
// A bare object id is a detached HEAD: there genuinely is no branch, which is different from
|
|
100
|
+
// failing to find one, and a reader deciding whether a run is on the branch it should be needs
|
|
101
|
+
// to be able to tell those apart.
|
|
102
|
+
if (/^[0-9a-f]{40}$/i.test(raw))
|
|
103
|
+
return { branch: null, reason: 'HEAD is detached' };
|
|
104
|
+
return { branch: null, reason: `HEAD is in an unrecognised form: ${raw.slice(0, 40)}` };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=git-facts.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the coverage table into the `options.hooks` object the SDK actually takes.
|
|
3
|
+
*
|
|
4
|
+
* Registration is derived from the table, not written beside it. The list of events registered
|
|
5
|
+
* here is the list `coverage.ts` marks `wired`, so the table cannot claim an event is wired while
|
|
6
|
+
* the wiring quietly lacks it. A table that can disagree with the code is a document that will.
|
|
7
|
+
*
|
|
8
|
+
* Every handler is wrapped, because a throwing hook is fail-open. A hook that throws is treated
|
|
9
|
+
* by the CLI as absent rather than as a refusal (observed behaviour, not an assumption). For an
|
|
10
|
+
* observer that would mean a lost transition and nothing else, which is precisely the silent loss
|
|
11
|
+
* this model exists to prevent, so the wrapper is here and not left to the caller.
|
|
12
|
+
*
|
|
13
|
+
* This handler never decides anything. It returns an empty output on every path, including the
|
|
14
|
+
* failure path. `HookCallbackMatcher.hooks` is an array and the SDK runs every entry, so a
|
|
15
|
+
* permission-decision handler registers on the same event alongside this one; neither has to know
|
|
16
|
+
* about the other, and observation cannot accidentally become authorization.
|
|
17
|
+
*/
|
|
18
|
+
import type { HookRegistrations } from './agent-process.js';
|
|
19
|
+
import type { SessionObserver } from '../state/observer.js';
|
|
20
|
+
/** Told about a handler that threw, so a fail-open hook is never a silent one. */
|
|
21
|
+
export type HookFailureListener = (failure: {
|
|
22
|
+
event: string;
|
|
23
|
+
error: unknown;
|
|
24
|
+
}) => void;
|
|
25
|
+
export interface ObservationHookOptions {
|
|
26
|
+
readonly observer: SessionObserver;
|
|
27
|
+
readonly onHandlerFailure?: HookFailureListener;
|
|
28
|
+
}
|
|
29
|
+
/** Every event the table marks `wired`, in the SDK's own order. */
|
|
30
|
+
export declare function wiredHookEvents(): readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* The `hooks` object for a session, registering exactly the wired events.
|
|
33
|
+
*
|
|
34
|
+
* No matcher is set: a matcher filters by tool name, and this observes every tool. The absence is
|
|
35
|
+
* deliberate rather than an omission.
|
|
36
|
+
*/
|
|
37
|
+
export declare function observationHooks(options: ObservationHookOptions): HookRegistrations;
|
|
38
|
+
/**
|
|
39
|
+
* Combine independent hook registrations, concatenating the matchers per event.
|
|
40
|
+
*
|
|
41
|
+
* This is the seam that keeps observation and authorization apart. A permission decision and a
|
|
42
|
+
* state record answer different questions, have different consumers and fail differently, and the
|
|
43
|
+
* failure this package is built against is exactly what happens when they share a guard: the
|
|
44
|
+
* control concern wins, and the observability loss is silent. Two matchers on one event, merged
|
|
45
|
+
* here, means neither can suppress the other because neither knows the other exists.
|
|
46
|
+
*
|
|
47
|
+
* "Earlier arguments run first" is true of dispatch and false of completion. Handlers on one
|
|
48
|
+
* event have their synchronous prologues run in array order, and are then awaited concurrently
|
|
49
|
+
* (observed behaviour, not an assumption). So a handler may rely on an earlier one having started
|
|
50
|
+
* and must never rely on it having finished: anything order-dependent belongs before the first
|
|
51
|
+
* `await`. (The
|
|
52
|
+
* gate's permission entry opens from its hold timer, not in its prologue, so it depends on no
|
|
53
|
+
* registration order; it registers second by convention.)
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergeHooks(...registrations: readonly HookRegistrations[]): HookRegistrations;
|
|
56
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HOOK_COVERAGE } from '../state/coverage.js';
|
|
2
|
+
import { HOOK_EVENTS } from '../state/model.js';
|
|
3
|
+
/** Every event the table marks `wired`, in the SDK's own order. */
|
|
4
|
+
export function wiredHookEvents() {
|
|
5
|
+
return HOOK_EVENTS.filter((event) => HOOK_COVERAGE[event].handling === 'wired');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The `hooks` object for a session, registering exactly the wired events.
|
|
9
|
+
*
|
|
10
|
+
* No matcher is set: a matcher filters by tool name, and this observes every tool. The absence is
|
|
11
|
+
* deliberate rather than an omission.
|
|
12
|
+
*/
|
|
13
|
+
export function observationHooks(options) {
|
|
14
|
+
const handler = (input) => {
|
|
15
|
+
try {
|
|
16
|
+
// The results are not read here. A transition the machine refuses is reported on
|
|
17
|
+
// `machine.onRejected` and counted in `rejectedCount`, the one channel every refused record
|
|
18
|
+
// takes whatever lane produced it; reporting it again from this handler would count one
|
|
19
|
+
// refusal twice. The observer builds a request only for an event it names, so an event this
|
|
20
|
+
// package does not know records nothing rather than producing a refusal to route.
|
|
21
|
+
options.observer.observeHook(input);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// A throw here would make the CLI treat the hook as absent, which is the fail-open hole. It
|
|
25
|
+
// is caught, reported, and never rethrown; losing one observation loudly beats losing the
|
|
26
|
+
// handler entirely and silently. The listener is guarded too: a reporter that throws must
|
|
27
|
+
// not reopen the hole it exists to report.
|
|
28
|
+
try {
|
|
29
|
+
options.onHandlerFailure?.({ event: input.hook_event_name, error });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Nothing further can be reported; the handler still answers.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Promise.resolve({});
|
|
36
|
+
};
|
|
37
|
+
// One matcher object per event. A shared instance would let a `timeout` or `matcher` set on one
|
|
38
|
+
// event's entry apply to every event.
|
|
39
|
+
const registrations = {};
|
|
40
|
+
for (const event of HOOK_EVENTS) {
|
|
41
|
+
if (HOOK_COVERAGE[event].handling !== 'wired')
|
|
42
|
+
continue;
|
|
43
|
+
const matcher = { hooks: [handler] };
|
|
44
|
+
registrations[event] = [matcher];
|
|
45
|
+
}
|
|
46
|
+
return registrations;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Combine independent hook registrations, concatenating the matchers per event.
|
|
50
|
+
*
|
|
51
|
+
* This is the seam that keeps observation and authorization apart. A permission decision and a
|
|
52
|
+
* state record answer different questions, have different consumers and fail differently, and the
|
|
53
|
+
* failure this package is built against is exactly what happens when they share a guard: the
|
|
54
|
+
* control concern wins, and the observability loss is silent. Two matchers on one event, merged
|
|
55
|
+
* here, means neither can suppress the other because neither knows the other exists.
|
|
56
|
+
*
|
|
57
|
+
* "Earlier arguments run first" is true of dispatch and false of completion. Handlers on one
|
|
58
|
+
* event have their synchronous prologues run in array order, and are then awaited concurrently
|
|
59
|
+
* (observed behaviour, not an assumption). So a handler may rely on an earlier one having started
|
|
60
|
+
* and must never rely on it having finished: anything order-dependent belongs before the first
|
|
61
|
+
* `await`. (The
|
|
62
|
+
* gate's permission entry opens from its hold timer, not in its prologue, so it depends on no
|
|
63
|
+
* registration order; it registers second by convention.)
|
|
64
|
+
*/
|
|
65
|
+
export function mergeHooks(...registrations) {
|
|
66
|
+
const merged = {};
|
|
67
|
+
for (const registration of registrations) {
|
|
68
|
+
for (const [event, matchers] of Object.entries(registration)) {
|
|
69
|
+
const key = event;
|
|
70
|
+
merged[key] = [...(merged[key] ?? []), ...(matchers ?? [])];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer: the file that makes the parts a host.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists: every part of this package is built and proven through its own seam, and
|
|
5
|
+
* something has to join them. Without this file a session created through the registry runs with
|
|
6
|
+
* `hooks: null` (no gate, no observation) and the link decodes `session_new`, `session_prompt`,
|
|
7
|
+
* `session_cancel` and `bulk_request` with nothing consuming them, so the host cannot be told to
|
|
8
|
+
* do anything at all.
|
|
9
|
+
*
|
|
10
|
+
* Two things live here and the split is the testability.
|
|
11
|
+
* `composeSession` is the per-session assembly (machine, observer, gate, forwarding) and it
|
|
12
|
+
* takes everything as arguments, so it can be checked without a socket or a process.
|
|
13
|
+
* `PeriscopeHost` owns the link and the registry and turns inbound payloads into calls on it.
|
|
14
|
+
*
|
|
15
|
+
* It lives in `src/host/` for the ordinary reason: it wires the real path resolver, the real bulk
|
|
16
|
+
* POST and the real MCP server, all of which are confined here. It imports no `node:` builtin of
|
|
17
|
+
* its own; the boundary is satisfied by construction rather than by permission.
|
|
18
|
+
*
|
|
19
|
+
* The order of assembly is load-bearing and it is not obvious. Forwarding is attached before the
|
|
20
|
+
* first transition is recorded, because the forwarder subscribes to the machine: attach it after
|
|
21
|
+
* and the `spawning` record (the one that carries where the session is and that it exists at all)
|
|
22
|
+
* is emitted to nobody. It is the first frame a controller ever sees for a session, so losing it
|
|
23
|
+
* costs the session's whole opening.
|
|
24
|
+
*/
|
|
25
|
+
import type { Clock, Ticker } from '../core/time.js';
|
|
26
|
+
import type { Refusal } from '../core/refusal.js';
|
|
27
|
+
import type { Result } from '../core/result.js';
|
|
28
|
+
import type { ControllerCredential } from '../control/credential.js';
|
|
29
|
+
import type { BackoffOptions } from '../control/backoff.js';
|
|
30
|
+
import type { LinkHandlers } from '../control/link.js';
|
|
31
|
+
import type { LinkTransition } from '../control/link-state.js';
|
|
32
|
+
import type { HostConfiguration, HostConfigureEntry } from '../control/frames.js';
|
|
33
|
+
import type { FrameSink } from '../control/stream.js';
|
|
34
|
+
import type { Decider } from '../gate/decision.js';
|
|
35
|
+
import type { LocalGate } from '../gate/local.js';
|
|
36
|
+
import type { ToolFamilies } from '../gate/local.js';
|
|
37
|
+
import type { GateOutcome } from '../gate/outcome.js';
|
|
38
|
+
import type { McpServerOptions } from '../mcp/server.js';
|
|
39
|
+
import type { SessionRequest } from '../sessions/registry.js';
|
|
40
|
+
import { SessionRegistry } from '../sessions/registry.js';
|
|
41
|
+
import type { HostedSession, SessionDegrade, Unsubscribe } from '../sessions/session.js';
|
|
42
|
+
import { SessionStateMachine } from '../state/machine.js';
|
|
43
|
+
import type { SessionTransition, TransitionWhere } from '../state/model.js';
|
|
44
|
+
import { SessionObserver } from '../state/observer.js';
|
|
45
|
+
import type { WorkspaceProvider } from '../workspace/provider.js';
|
|
46
|
+
/** How long the gate waits, when it reports a call as held, and whether its allow takes effect. */
|
|
47
|
+
export interface GateTimings {
|
|
48
|
+
readonly decisionTimeoutMs?: number;
|
|
49
|
+
readonly holdAfterMs?: number;
|
|
50
|
+
readonly matcherTimeoutSeconds?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Make the gate's allow effective.
|
|
53
|
+
*
|
|
54
|
+
* Defaults to false, and leaving it there gives a gate that cannot say yes. Without it
|
|
55
|
+
* this gate is a veto: it can refuse a call and cannot let one through. Observed on a real
|
|
56
|
+
* session: the gate allowed a `Write`, the tool did not run, and the model was told "Claude
|
|
57
|
+
* requested permissions to write to ..., but you haven't granted it yet", in a host with no user
|
|
58
|
+
* to grant anything. `PeriscopeHost` sets it to `true` for exactly this reason; an embedder
|
|
59
|
+
* calling `composeSession` by hand does not.
|
|
60
|
+
*
|
|
61
|
+
* The first allow that does not take effect raises a `gate-cannot-grant` degrade (see
|
|
62
|
+
* `permissionHooks`), so an embedder who left it off is told by a named outcome rather than by
|
|
63
|
+
* a comment.
|
|
64
|
+
*
|
|
65
|
+
* Setting it with `settingSources` non-empty is refused (`permission-grant-shadows-settings`).
|
|
66
|
+
* Per the SDK's documented evaluation order a hook allow does not skip operator deny or ask
|
|
67
|
+
* rules (it skips permission mode, allow rules and `canUseTool`), so the exposure is narrower
|
|
68
|
+
* than a bypass. See `composeSession` for what the refusal does and does not cover.
|
|
69
|
+
*/
|
|
70
|
+
readonly grantOnAllow?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface ComposeSessionOptions {
|
|
73
|
+
readonly registry: SessionRegistry;
|
|
74
|
+
/** The controller's handle: the frame routing key, not the agent's id. See frames.ts. */
|
|
75
|
+
readonly sessionKey: string;
|
|
76
|
+
/** Where the session runs. Absolute; the registry refuses anything else. */
|
|
77
|
+
readonly cwd: string;
|
|
78
|
+
readonly sink: FrameSink;
|
|
79
|
+
/** Who answers a permission escalation. Usually `escalatingDecider`. */
|
|
80
|
+
readonly decide: Decider;
|
|
81
|
+
/**
|
|
82
|
+
* The host's own gate, consulted before the decider.
|
|
83
|
+
*
|
|
84
|
+
* Optional in the type, supplied by default by `PeriscopeHost`. Absent means a path escape or a
|
|
85
|
+
* credential read waits `decisionTimeoutMs` for an unreachable controller and is reported as an
|
|
86
|
+
* outage, instead of being refused locally by name; see `gate/local.ts`. Composing without one
|
|
87
|
+
* is a decision, so it stays possible and is never the default.
|
|
88
|
+
*/
|
|
89
|
+
readonly localGate?: LocalGate;
|
|
90
|
+
readonly gate?: GateTimings;
|
|
91
|
+
/** Read once, at composition. Carried by every transition until the agent moves itself. */
|
|
92
|
+
readonly where?: TransitionWhere;
|
|
93
|
+
/** Opaque and never interpreted here. The controller's own meaning handle. */
|
|
94
|
+
readonly correlationId?: string | null;
|
|
95
|
+
/** Everything else the session takes — MCP servers, a store, plugins, resume. */
|
|
96
|
+
readonly request?: Omit<SessionRequest, 'cwd' | 'hooks'>;
|
|
97
|
+
readonly clock?: Clock;
|
|
98
|
+
readonly ticker?: Ticker;
|
|
99
|
+
/** Every refusal the sink returned, every transition the machine would not record. */
|
|
100
|
+
readonly onRefusal?: (refused: Refusal) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Told about every gate outcome, in addition to the machine record, never instead of it.
|
|
103
|
+
*
|
|
104
|
+
* The record is what reaches the wire and it is not optional; this is for an embedder that also
|
|
105
|
+
* wants to count, meter or log outcomes locally.
|
|
106
|
+
*/
|
|
107
|
+
readonly onOutcome?: (outcome: GateOutcome) => void;
|
|
108
|
+
/** A hook handler that threw. The CLI treats a throwing hook as absent, so it is never silent. */
|
|
109
|
+
readonly onHookFailure?: (failure: {
|
|
110
|
+
event: string;
|
|
111
|
+
error: unknown;
|
|
112
|
+
}) => void;
|
|
113
|
+
}
|
|
114
|
+
/** One composed session: the handle, its machine, and how to take the wiring back down. */
|
|
115
|
+
export interface ComposedSession {
|
|
116
|
+
readonly sessionKey: string;
|
|
117
|
+
readonly session: HostedSession;
|
|
118
|
+
readonly machine: SessionStateMachine;
|
|
119
|
+
readonly observer: SessionObserver;
|
|
120
|
+
/** Stops forwarding. Idempotent. Does not end the session; the registry owns lifetime. */
|
|
121
|
+
readonly detach: Unsubscribe;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Assemble one session: machine, observer, gate and forwarding, then start it.
|
|
125
|
+
*
|
|
126
|
+
* This is the wiring every consumer needs, written down where it can be used.
|
|
127
|
+
*/
|
|
128
|
+
export declare function composeSession(options: ComposeSessionOptions): Result<ComposedSession>;
|
|
129
|
+
/** Turns the opaque `what` of a bulk request into a file this host will POST. */
|
|
130
|
+
export type BulkResolver = (what: string, sessionKey: string) => Result<string>;
|
|
131
|
+
/**
|
|
132
|
+
* The link surface a host uses. `ControllerLink` satisfies it.
|
|
133
|
+
*
|
|
134
|
+
* It is structural so the host's own rules can be checked without a socket: that an unknown
|
|
135
|
+
* handle is refused rather than dropped, that two `session_new` for one handle do not silently
|
|
136
|
+
* replace each other, that a failed delivery still sends a receipt, that a finished session is
|
|
137
|
+
* released in an order which keeps its last frames. It is not a way to test the transport: a
|
|
138
|
+
* substitute proves the substitute, and everything this package claims about reconnect, replay and
|
|
139
|
+
* `seq` is proven against the real link in `control/`.
|
|
140
|
+
*/
|
|
141
|
+
export interface HostLink extends FrameSink {
|
|
142
|
+
start(): void;
|
|
143
|
+
stop(detail?: string): void;
|
|
144
|
+
forgetSession(sessionId: string): void;
|
|
145
|
+
/** The version the controller chose at the last accepted handshake; optional because a test link negotiates nothing. */
|
|
146
|
+
readonly negotiatedVersion?: number | null;
|
|
147
|
+
/** Replace what the next `link_hello` declares; optional because a test link declares nothing. */
|
|
148
|
+
announce?(capabilities: readonly string[], configuration: HostConfiguration, pendingRestart: readonly string[]): void;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* What a reconfigure produced: the pieces the host swaps in place of the ones it was composed with.
|
|
152
|
+
* `undefined` members mean the same as an omitted option at composition (no provider, no root).
|
|
153
|
+
*/
|
|
154
|
+
export interface HostReconfigured {
|
|
155
|
+
readonly workspaces: WorkspaceProvider | undefined;
|
|
156
|
+
readonly transcriptsRoot: string | undefined;
|
|
157
|
+
readonly bulk: BulkResolver | undefined;
|
|
158
|
+
readonly linkCapabilities: readonly string[];
|
|
159
|
+
readonly configuration: HostConfiguration;
|
|
160
|
+
readonly overriddenByEnvironment: readonly string[];
|
|
161
|
+
/** The keys written but not in effect until the next start. */
|
|
162
|
+
readonly pendingRestart: readonly string[];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The configuration seam a `host_configure` ask runs through. The composition root supplies it,
|
|
166
|
+
* because writing the config file and rebuilding a provider both read the environment, which is
|
|
167
|
+
* that file's job alone. `hostBusy` is true while any session is live or opening; the seam refuses
|
|
168
|
+
* a roots change by name in that state rather than writing a file the running host cannot honour.
|
|
169
|
+
*/
|
|
170
|
+
export type HostReconfigurer = (entries: readonly HostConfigureEntry[], hostBusy: boolean) => Result<HostReconfigured>;
|
|
171
|
+
export interface PeriscopeHostOptions {
|
|
172
|
+
readonly controllerUrl: string;
|
|
173
|
+
readonly hostId: string;
|
|
174
|
+
/**
|
|
175
|
+
* The identity this host presents outbound, on the link's upgrade and on every bulk POST
|
|
176
|
+
* (resolved per delivery; a refusing credential posts headerless, and the receiver's refusal
|
|
177
|
+
* comes back as `bulk-delivery-failed`). Omitted means every outbound surface is anonymous.
|
|
178
|
+
*/
|
|
179
|
+
readonly credential?: ControllerCredential;
|
|
180
|
+
readonly backoff?: BackoffOptions;
|
|
181
|
+
/**
|
|
182
|
+
* The default link's clocks: the heartbeat's interval and timeout, and how long a dial may sit
|
|
183
|
+
* without an open before it is abandoned and retried. Ignored when `link` supplies the link.
|
|
184
|
+
*/
|
|
185
|
+
readonly linkTimings?: {
|
|
186
|
+
readonly heartbeatIntervalMs?: number;
|
|
187
|
+
readonly heartbeatTimeoutMs?: number;
|
|
188
|
+
readonly connectTimeoutMs?: number;
|
|
189
|
+
};
|
|
190
|
+
/** Who answers a permission escalation. Required: a host with no decider gates nothing. */
|
|
191
|
+
readonly decide: Decider;
|
|
192
|
+
/**
|
|
193
|
+
* Absolute paths the agent may never read or write, whatever its workspace.
|
|
194
|
+
*
|
|
195
|
+
* `host/paths.ts`'s `credentialPaths(env)` computes the default set; the composition root passes
|
|
196
|
+
* it in, because reading the environment is that file's job and only that file's.
|
|
197
|
+
*/
|
|
198
|
+
readonly protectedPaths: readonly string[];
|
|
199
|
+
/**
|
|
200
|
+
* Which tool names the local gate treats as path writes, path reads and shell commands.
|
|
201
|
+
*
|
|
202
|
+
* Defaults to the SDK's own tools (`DEFAULT_TOOL_FAMILIES`). An MCP tool the embedder registers
|
|
203
|
+
* arrives as `mcp__{server}__{tool}`, matches nothing, and escalates with no local opinion; naming
|
|
204
|
+
* it here gives it the same local treatment as the built-in it resembles. Data, never guessed:
|
|
205
|
+
* the host cannot know which of an embedder's tools are dangerous.
|
|
206
|
+
*/
|
|
207
|
+
readonly toolFamilies?: ToolFamilies;
|
|
208
|
+
/**
|
|
209
|
+
* Where sessions run. With a provider the controller's `cwd` is advisory: the provider decides,
|
|
210
|
+
* and the session's `spawning` transition carries the directory it actually got, so the
|
|
211
|
+
* controller learns where its session is rather than assuming.
|
|
212
|
+
*/
|
|
213
|
+
readonly workspaces?: WorkspaceProvider;
|
|
214
|
+
/**
|
|
215
|
+
* The key an unkeyed `session_new` provisions at, when it should not be the session key
|
|
216
|
+
* (protocol v5).
|
|
217
|
+
*
|
|
218
|
+
* A default for an absence, never an override: resolution is exactly
|
|
219
|
+
* `opening.workspaceKey ?? defaultWorkspaceKey ?? sessionKey`, so a controller that names a key
|
|
220
|
+
* never has this consulted and no precedence question exists. It is what lets a library user
|
|
221
|
+
* with no controller at all get a shared tree: every unkeyed session lands in one workspace.
|
|
222
|
+
* The composition root screens the configured value at startup (same union screen as a wire
|
|
223
|
+
* key); the per-frame validation below still covers an embedder that passes one here directly.
|
|
224
|
+
*/
|
|
225
|
+
readonly defaultWorkspaceKey?: string;
|
|
226
|
+
/**
|
|
227
|
+
* In-process tools this host offers every session. See the note on registration below.
|
|
228
|
+
*
|
|
229
|
+
* `identity` is not the embedder's to supply: it must be read at CALL time and only this host
|
|
230
|
+
* knows which session is calling, so it is filled in here and omitted from the type rather than
|
|
231
|
+
* accepted and ignored.
|
|
232
|
+
*/
|
|
233
|
+
readonly tools?: Omit<McpServerOptions, 'identity'>;
|
|
234
|
+
readonly bulk?: BulkResolver;
|
|
235
|
+
/**
|
|
236
|
+
* Where the agent CLI's transcripts live (`~/.claude/projects`), for the discovery door.
|
|
237
|
+
*
|
|
238
|
+
* The composition root derives it (`claudeProjectsRoot(env)` in `host/claude-transcripts.ts`),
|
|
239
|
+
* because reading the environment is that layer's job. Absent means the door answers every
|
|
240
|
+
* `transcript_list` / `transcript_tail` with a named failure rather than guessing a root;
|
|
241
|
+
* `session_list` needs no filesystem and always answers.
|
|
242
|
+
*/
|
|
243
|
+
readonly transcriptsRoot?: string;
|
|
244
|
+
readonly gate?: GateTimings;
|
|
245
|
+
/**
|
|
246
|
+
* Capability markers this host declares in its `link_hello`, beside the built-in `bulk-post`,
|
|
247
|
+
* never replacing it.
|
|
248
|
+
*
|
|
249
|
+
* The workspace mode rides here: the composition root computes
|
|
250
|
+
* `workspaceCapabilitiesOf(config)` (`bin/workspaces.ts`) because only it knows which provider
|
|
251
|
+
* it chose; the mode is deliberately erased from `WorkspaceProvider` itself. Markers are values
|
|
252
|
+
* in an existing open string list, so declaring a new one is not a protocol change and bumps no
|
|
253
|
+
* version; a controller that does not know a marker ignores it by construction. Values a marker
|
|
254
|
+
* cannot carry (which repository root, which scheme text) are a payload-member conversation, a
|
|
255
|
+
* version bump, and deliberately not smuggled in here.
|
|
256
|
+
*/
|
|
257
|
+
readonly linkCapabilities?: readonly string[];
|
|
258
|
+
/**
|
|
259
|
+
* The values behind those markers: which roots, which scheme, which transcripts root,
|
|
260
|
+
* which controller. The composition root computes `hostConfigurationOf(config, ...)` beside the
|
|
261
|
+
* markers, from the same reading, so the two cannot disagree. Omitted, the hello reports every
|
|
262
|
+
* value as null.
|
|
263
|
+
*/
|
|
264
|
+
readonly configuration?: HostConfiguration;
|
|
265
|
+
/** The wire-settable keys the environment sets, reported on a configure answer. */
|
|
266
|
+
readonly overriddenByEnvironment?: readonly string[];
|
|
267
|
+
/** The keys the file names differently from what this process dialled; empty at a fresh start. */
|
|
268
|
+
readonly pendingRestart?: readonly string[];
|
|
269
|
+
/** How a `host_configure` ask is applied. Omitted, every such ask refuses `config-write-failed`. */
|
|
270
|
+
readonly reconfigure?: HostReconfigurer;
|
|
271
|
+
readonly registry?: SessionRegistry;
|
|
272
|
+
/** How the link is built. Defaults to the real `ControllerLink`. See `HostLink`. */
|
|
273
|
+
readonly link?: (handlers: LinkHandlers) => HostLink;
|
|
274
|
+
/** The environment sessions are filtered from, when this builds its own registry. */
|
|
275
|
+
readonly baseEnv?: Readonly<Record<string, string | undefined>>;
|
|
276
|
+
readonly homeDir?: string;
|
|
277
|
+
readonly clock?: Clock;
|
|
278
|
+
readonly ticker?: Ticker;
|
|
279
|
+
/** Every named thing that happened. A host with no reporter is a host nobody can debug. */
|
|
280
|
+
readonly report?: (event: HostEvent) => void;
|
|
281
|
+
}
|
|
282
|
+
/** What a host reports. Data rather than log lines, so an embedder decides the format. */
|
|
283
|
+
export type HostEvent = {
|
|
284
|
+
readonly kind: 'link';
|
|
285
|
+
readonly transition: LinkTransition;
|
|
286
|
+
} | {
|
|
287
|
+
readonly kind: 'refusal';
|
|
288
|
+
readonly refusal: Refusal;
|
|
289
|
+
readonly sessionKey: string | null;
|
|
290
|
+
} | {
|
|
291
|
+
readonly kind: 'gap';
|
|
292
|
+
readonly sessionKey: string;
|
|
293
|
+
readonly expected: number;
|
|
294
|
+
readonly received: number;
|
|
295
|
+
} | {
|
|
296
|
+
readonly kind: 'session-opened';
|
|
297
|
+
readonly sessionKey: string;
|
|
298
|
+
readonly cwd: string;
|
|
299
|
+
} | {
|
|
300
|
+
readonly kind: 'session-closed';
|
|
301
|
+
readonly sessionKey: string;
|
|
302
|
+
} | {
|
|
303
|
+
readonly kind: 'prompt-held';
|
|
304
|
+
readonly sessionKey: string;
|
|
305
|
+
readonly held: number;
|
|
306
|
+
} | {
|
|
307
|
+
readonly kind: 'prompt-delivered';
|
|
308
|
+
readonly sessionKey: string;
|
|
309
|
+
readonly delivered: number;
|
|
310
|
+
} | {
|
|
311
|
+
readonly kind: 'prompt-withdrawn';
|
|
312
|
+
readonly sessionKey: string;
|
|
313
|
+
readonly withdrawn: number;
|
|
314
|
+
} | {
|
|
315
|
+
readonly kind: 'transition';
|
|
316
|
+
readonly sessionKey: string;
|
|
317
|
+
readonly transition: SessionTransition;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* A named condition that changed what is true of a session without ending it.
|
|
321
|
+
*
|
|
322
|
+
* Every degrade the sessions layer raises arrives here. The registry names its conditions (an
|
|
323
|
+
* untrusted workspace whose settings rules are silently void, an id collision whose own detail
|
|
324
|
+
* says how to resolve it) into a listener set, and a listener set with no subscribers would
|
|
325
|
+
* mean the one message that answers the operator's question is raised and never received. The
|
|
326
|
+
* subscription in `#compose` is the audience; a pin in `host.test.ts` holds every kind to it.
|
|
327
|
+
*/
|
|
328
|
+
| {
|
|
329
|
+
readonly kind: 'degrade';
|
|
330
|
+
readonly sessionKey: string;
|
|
331
|
+
readonly degrade: SessionDegrade;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* A host: one outbound link, one registry, and the dispatcher between them.
|
|
335
|
+
*
|
|
336
|
+
* It interprets exactly the payload kinds its `#dispatch` switch names and refuses the rest by
|
|
337
|
+
* name: the session commands (`session_new`, `session_prompt`, `session_cancel`,
|
|
338
|
+
* `session_configure`), `bulk_request`, and the host-scoped asks (the discovery, workspace,
|
|
339
|
+
* configure and repository asks). Derive the count from the switch below; never carry it from
|
|
340
|
+
* prose. Anything else arriving inbound is
|
|
341
|
+
* either a frame this host produces (an update, a delta, a receipt) or a kind a newer controller
|
|
342
|
+
* invented, and both are reported rather than ignored; a command that vanishes reads to the
|
|
343
|
+
* controller as a host that hung.
|
|
344
|
+
*/
|
|
345
|
+
export declare class PeriscopeHost {
|
|
346
|
+
#private;
|
|
347
|
+
constructor(options: PeriscopeHostOptions);
|
|
348
|
+
get link(): HostLink;
|
|
349
|
+
get registry(): SessionRegistry;
|
|
350
|
+
/** The composed session behind a controller handle, or a refusal naming the handle. */
|
|
351
|
+
session(sessionKey: string): Result<ComposedSession>;
|
|
352
|
+
start(): void;
|
|
353
|
+
/** Ends every session first, then the link — so the end transitions still have somewhere to go. */
|
|
354
|
+
stop(detail?: string): void;
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=host.d.ts.map
|