@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,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory a session runs in, and who decides what it looks like.
|
|
3
|
+
*
|
|
4
|
+
* This is entirely the host's job. The SDK has no provisioning surface at all — it takes a `cwd`
|
|
5
|
+
* and assumes something already made it. So the choice of what a workspace is — a plain directory, a
|
|
6
|
+
* checkout, a linked worktree, a container mount — is the embedder's, and this is the seam where
|
|
7
|
+
* that choice plugs in.
|
|
8
|
+
*
|
|
9
|
+
* The provider is the embedder's, not the package's. Two implementations ship because they cover
|
|
10
|
+
* the two shapes almost everyone needs, but the interesting policies are not here and must not move
|
|
11
|
+
* here: a shared directory per task, a write-once key, a branch naming rule, a cleanup schedule.
|
|
12
|
+
* Those are the embedder's own conventions, and a tool that reviews code in a throwaway clone
|
|
13
|
+
* should be able to write its own provider without ever learning that any of them exist.
|
|
14
|
+
*
|
|
15
|
+
* Every result is a named refusal, never an exception. `provision` failing is an ordinary outcome
|
|
16
|
+
* — a disk is full, a path is taken, a git command failed — and the caller has to decide what to do
|
|
17
|
+
* about it. A thrown error at this seam would make "the session could not get a directory"
|
|
18
|
+
* indistinguishable from a bug in the provider, and the package names the difference everywhere else.
|
|
19
|
+
*/
|
|
20
|
+
import type { Result } from '../core/result.js';
|
|
21
|
+
import type { Refusal } from '../core/refusal.js';
|
|
22
|
+
/** Where a session works, plus whatever the provider wants the caller to know about it. */
|
|
23
|
+
export interface Workspace {
|
|
24
|
+
/** Absolute, always. This is what becomes the session's `cwd`. */
|
|
25
|
+
readonly path: string;
|
|
26
|
+
/**
|
|
27
|
+
* Provider-specific facts, as strings.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately opaque to this package. A git provider reports its branch and base ref here; a
|
|
30
|
+
* plain-directory provider reports almost nothing; somebody else's reports a container id. Nothing
|
|
31
|
+
* in the host reads a key out of this — it exists so a provider can tell its caller something
|
|
32
|
+
* without this file having to grow a field for every provider anyone might write.
|
|
33
|
+
*/
|
|
34
|
+
readonly meta: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
/** What `release` should do with the directory. */
|
|
37
|
+
export interface ReleaseOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Remove the workspace from disk. Defaults to false.
|
|
40
|
+
*
|
|
41
|
+
* The default is "leave it", and that is a deliberate asymmetry. A session that ended badly is
|
|
42
|
+
* one whose directory somebody wants to look at, and a host that tidied up by default would
|
|
43
|
+
* destroy the evidence exactly when it matters. Leaving a directory costs disk; deleting one that
|
|
44
|
+
* was still wanted costs the investigation.
|
|
45
|
+
*/
|
|
46
|
+
readonly remove?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Delete the branch the worktree is on, after the directory. Defaults to false, and only means
|
|
49
|
+
* anything with `remove`. Deleting a branch is not reversible the way removing a directory is,
|
|
50
|
+
* so it is asked for per call and never remembered.
|
|
51
|
+
*/
|
|
52
|
+
readonly deleteBranch?: boolean;
|
|
53
|
+
/** Delete the branch even when it is not merged into the repository's default branch. */
|
|
54
|
+
readonly force?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What a release did. `refusal` is the partial: the directory went and the branch did not, said
|
|
58
|
+
* rather than hidden. Null means everything asked for happened, or was already absent.
|
|
59
|
+
*/
|
|
60
|
+
export interface ReleaseReceipt {
|
|
61
|
+
/** The workspace's path as this provider knows it. */
|
|
62
|
+
readonly path: string;
|
|
63
|
+
/** A directory existed and is gone. */
|
|
64
|
+
readonly directoryRemoved: boolean;
|
|
65
|
+
readonly branchDeleted: boolean;
|
|
66
|
+
readonly refusal: Refusal | null;
|
|
67
|
+
}
|
|
68
|
+
/** One workspace on disk, as the provider's inventory reports it. See `WorkspaceListEntry` on the wire. */
|
|
69
|
+
export interface WorkspaceEntry {
|
|
70
|
+
readonly key: string;
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly branch: string | null;
|
|
73
|
+
readonly head: string | null;
|
|
74
|
+
readonly detached: boolean;
|
|
75
|
+
readonly locked: boolean;
|
|
76
|
+
readonly prunable: boolean;
|
|
77
|
+
readonly merged: boolean | null;
|
|
78
|
+
/** Commits past the default branch; null when detached or when no default branch was named. */
|
|
79
|
+
readonly aheadCount: number | null;
|
|
80
|
+
readonly lastCommitAt: string | null;
|
|
81
|
+
}
|
|
82
|
+
/** What `inventory` answers: every workspace under the root, and the default branch `merged` was judged against. */
|
|
83
|
+
export interface WorkspaceInventory {
|
|
84
|
+
readonly entries: readonly WorkspaceEntry[];
|
|
85
|
+
readonly defaultBranch: string | null;
|
|
86
|
+
}
|
|
87
|
+
export interface WorkspaceProvider {
|
|
88
|
+
/**
|
|
89
|
+
* Get the directory for this session, creating it if needed.
|
|
90
|
+
*
|
|
91
|
+
* It must be safe to call twice for one session. A host that lost a directory — a cleanup ran,
|
|
92
|
+
* a volume remounted, a container restarted — re-provisions, and the second call must attach to
|
|
93
|
+
* whatever survived rather than recreate it. That is not a nicety: see git-worktree.ts, where
|
|
94
|
+
* getting it wrong destroys committed work.
|
|
95
|
+
*/
|
|
96
|
+
provision(sessionId: string): Promise<Result<Workspace>>;
|
|
97
|
+
/**
|
|
98
|
+
* Done with it. Idempotent: releasing a session this provider does not hold succeeds. The
|
|
99
|
+
* receipt is optional so a provider that predates it still conforms; the host reads no receipt as
|
|
100
|
+
* "vouches for nothing" and reports both flags false.
|
|
101
|
+
*/
|
|
102
|
+
release(sessionId: string, options?: ReleaseOptions): Promise<Result<ReleaseReceipt | undefined>>;
|
|
103
|
+
/**
|
|
104
|
+
* The key of the workspace at `path`, when `path` is a directory directly under this provider's
|
|
105
|
+
* root; null for anything else. Optional: a provider without it cannot be addressed by path.
|
|
106
|
+
*/
|
|
107
|
+
keyForPath?(path: string): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* What exists under the workspace root right now, read from disk on demand. Optional: a provider
|
|
110
|
+
* that keeps no inventory leaves it out and the host answers `workspace-list-failed` by name.
|
|
111
|
+
* Only what this provider provisioned is listed; the repository it links from never is.
|
|
112
|
+
*/
|
|
113
|
+
inventory?(): Promise<Result<WorkspaceInventory>>;
|
|
114
|
+
/**
|
|
115
|
+
* The repository this provider clones from, when it has one. The one directory a controller may
|
|
116
|
+
* name and get, provider or not: the operator's own checkout has no isolation to protect.
|
|
117
|
+
*/
|
|
118
|
+
readonly repositoryRoot?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The filesystem and process effects a provider needs, as injected functions.
|
|
122
|
+
*
|
|
123
|
+
* Why this exists rather than a direct `node:fs` import. `src/host/` is the only directory allowed
|
|
124
|
+
* to touch the machine (pinned by pins/host-boundary.test.ts), and the provider logic — which argv a
|
|
125
|
+
* git call gets, when a directory is reused rather than made, what a release does — is exactly the
|
|
126
|
+
* part worth testing without a disk. So the logic lives here and the real implementations live in
|
|
127
|
+
* host/workspace-fs.ts, the same split the path jail already uses.
|
|
128
|
+
*
|
|
129
|
+
* The payoff is not tidiness: it makes "never hard-reset an existing branch" a pure unit test rather
|
|
130
|
+
* than something only a destroyed branch could have proven.
|
|
131
|
+
*/
|
|
132
|
+
export interface WorkspaceEffects {
|
|
133
|
+
/** Create a directory and every missing parent. Succeeds if it already exists. */
|
|
134
|
+
makeDirectory(path: string): Promise<void>;
|
|
135
|
+
/** Does anything exist at this path? */
|
|
136
|
+
exists(path: string): Promise<boolean>;
|
|
137
|
+
/** Remove a directory and everything under it. Succeeds if it is already gone. */
|
|
138
|
+
removeDirectory(path: string): Promise<void>;
|
|
139
|
+
/** The names of the directories directly under `path`, or an empty list when it does not exist. Optional. */
|
|
140
|
+
listDirectories?(path: string): Promise<string[]>;
|
|
141
|
+
}
|
|
142
|
+
/** Running a command. Separate from the filesystem effects because only one provider needs it. */
|
|
143
|
+
export interface CommandEffects {
|
|
144
|
+
/**
|
|
145
|
+
* Run a program with an argv array and return its stdout, trimmed.
|
|
146
|
+
*
|
|
147
|
+
* Argv, never a command string. A shell string would make every branch name and path an
|
|
148
|
+
* injection site, and branch names come from a caller this package does not control.
|
|
149
|
+
*/
|
|
150
|
+
run(program: string, args: readonly string[], cwd: string): Promise<string>;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readers for the three git outputs an inventory is built from. Pure: text in, records out, no
|
|
3
|
+
* process and no filesystem, so the parsing is testable against fixtures and the provider that
|
|
4
|
+
* runs the commands stays thin.
|
|
5
|
+
*
|
|
6
|
+
* `git worktree list --porcelain` prints one block per worktree, blank-line separated, each line
|
|
7
|
+
* `<attribute> <value>` or a bare attribute: `worktree <path>`, `HEAD <sha>`, `branch <ref>`,
|
|
8
|
+
* `detached`, `locked [reason]`, `prunable [reason]`, `bare`. Attributes this reader does not know
|
|
9
|
+
* are carried past, never fatal, so a newer git cannot break the inventory by adding one.
|
|
10
|
+
*/
|
|
11
|
+
/** One worktree as git reports it. `branch` is the short name (`refs/heads/` stripped), null when detached. */
|
|
12
|
+
export interface PorcelainWorktree {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly head: string | null;
|
|
15
|
+
readonly branch: string | null;
|
|
16
|
+
readonly detached: boolean;
|
|
17
|
+
readonly locked: boolean;
|
|
18
|
+
readonly prunable: boolean;
|
|
19
|
+
readonly bare: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseWorktreePorcelain(text: string): PorcelainWorktree[];
|
|
22
|
+
/**
|
|
23
|
+
* `git for-each-ref --format='%(refname:short)%09%(committerdate:iso-strict)' refs/heads/`: one
|
|
24
|
+
* branch per line, tab-separated. Returns the tip's committer date per branch, as given.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseBranchTips(text: string): Map<string, string>;
|
|
27
|
+
/** `git branch --merged <ref> --format='%(refname:short)'`: one branch per line. */
|
|
28
|
+
export declare function parseBranchList(text: string): Set<string>;
|
|
29
|
+
//# sourceMappingURL=worktree-porcelain.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readers for the three git outputs an inventory is built from. Pure: text in, records out, no
|
|
3
|
+
* process and no filesystem, so the parsing is testable against fixtures and the provider that
|
|
4
|
+
* runs the commands stays thin.
|
|
5
|
+
*
|
|
6
|
+
* `git worktree list --porcelain` prints one block per worktree, blank-line separated, each line
|
|
7
|
+
* `<attribute> <value>` or a bare attribute: `worktree <path>`, `HEAD <sha>`, `branch <ref>`,
|
|
8
|
+
* `detached`, `locked [reason]`, `prunable [reason]`, `bare`. Attributes this reader does not know
|
|
9
|
+
* are carried past, never fatal, so a newer git cannot break the inventory by adding one.
|
|
10
|
+
*/
|
|
11
|
+
const HEADS_PREFIX = 'refs/heads/';
|
|
12
|
+
export function parseWorktreePorcelain(text) {
|
|
13
|
+
const worktrees = [];
|
|
14
|
+
let current = null;
|
|
15
|
+
const flush = () => {
|
|
16
|
+
if (current !== null)
|
|
17
|
+
worktrees.push({ ...current });
|
|
18
|
+
current = null;
|
|
19
|
+
};
|
|
20
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
21
|
+
const line = raw.trimEnd();
|
|
22
|
+
if (line === '') {
|
|
23
|
+
flush();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const space = line.indexOf(' ');
|
|
27
|
+
const attribute = space === -1 ? line : line.slice(0, space);
|
|
28
|
+
const value = space === -1 ? '' : line.slice(space + 1);
|
|
29
|
+
if (attribute === 'worktree') {
|
|
30
|
+
flush();
|
|
31
|
+
current = {
|
|
32
|
+
path: value,
|
|
33
|
+
head: null,
|
|
34
|
+
branch: null,
|
|
35
|
+
detached: false,
|
|
36
|
+
locked: false,
|
|
37
|
+
prunable: false,
|
|
38
|
+
bare: false,
|
|
39
|
+
};
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (current === null)
|
|
43
|
+
continue;
|
|
44
|
+
switch (attribute) {
|
|
45
|
+
case 'HEAD':
|
|
46
|
+
current.head = value === '' ? null : value;
|
|
47
|
+
break;
|
|
48
|
+
case 'branch':
|
|
49
|
+
current.branch = value.startsWith(HEADS_PREFIX) ? value.slice(HEADS_PREFIX.length) : value;
|
|
50
|
+
break;
|
|
51
|
+
case 'detached':
|
|
52
|
+
current.detached = true;
|
|
53
|
+
break;
|
|
54
|
+
case 'locked':
|
|
55
|
+
current.locked = true;
|
|
56
|
+
break;
|
|
57
|
+
case 'prunable':
|
|
58
|
+
current.prunable = true;
|
|
59
|
+
break;
|
|
60
|
+
case 'bare':
|
|
61
|
+
current.bare = true;
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
flush();
|
|
68
|
+
return worktrees;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `git for-each-ref --format='%(refname:short)%09%(committerdate:iso-strict)' refs/heads/`: one
|
|
72
|
+
* branch per line, tab-separated. Returns the tip's committer date per branch, as given.
|
|
73
|
+
*/
|
|
74
|
+
export function parseBranchTips(text) {
|
|
75
|
+
const tips = new Map();
|
|
76
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
77
|
+
const line = raw.trim();
|
|
78
|
+
if (line === '')
|
|
79
|
+
continue;
|
|
80
|
+
const tab = line.indexOf('\t');
|
|
81
|
+
if (tab === -1)
|
|
82
|
+
continue;
|
|
83
|
+
const branch = line.slice(0, tab).trim();
|
|
84
|
+
const at = line.slice(tab + 1).trim();
|
|
85
|
+
if (branch !== '' && at !== '')
|
|
86
|
+
tips.set(branch, at);
|
|
87
|
+
}
|
|
88
|
+
return tips;
|
|
89
|
+
}
|
|
90
|
+
/** `git branch --merged <ref> --format='%(refname:short)'`: one branch per line. */
|
|
91
|
+
export function parseBranchList(text) {
|
|
92
|
+
const branches = new Set();
|
|
93
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
94
|
+
const line = raw.trim().replace(/^[*+]\s+/, '');
|
|
95
|
+
if (line !== '')
|
|
96
|
+
branches.add(line);
|
|
97
|
+
}
|
|
98
|
+
return branches;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=worktree-porcelain.js.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
What the package is and how its parts compose. The wire is in [protocol.md](protocol.md), the
|
|
4
|
+
session's state model in [state-machine.md](state-machine.md), the permission gate in
|
|
5
|
+
[gate.md](gate.md).
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
Periscope runs where the code lives, hosts Claude Code sessions through the Claude Agent SDK, and
|
|
10
|
+
reports to a controller anywhere else over one outbound WebSocket. The controller decides what a
|
|
11
|
+
session means; the host carries what it did. Three properties follow, each enforced rather than
|
|
12
|
+
promised:
|
|
13
|
+
|
|
14
|
+
- Generic. Nothing on the wire is shaped to one product. The test for a new member or event: could
|
|
15
|
+
a different controller make different meaning from it? If yes it belongs here; if it means
|
|
16
|
+
something only under one controller's rules it stays with that controller.
|
|
17
|
+
- The SDK's words. The package says session, message, hook, tool, subagent, turn, result and
|
|
18
|
+
permission as the SDK does. A controller's own vocabulary fails the build
|
|
19
|
+
(`pins/vocabulary.test.ts`). One site translates SDK events into transitions:
|
|
20
|
+
`src/state/observer.ts`.
|
|
21
|
+
- Auditable blast radius. Nothing outside `src/host/` imports `node:fs`, `node:fs/promises`,
|
|
22
|
+
`node:child_process` or `node:os`, and nothing outside it names the SDK (ESLint
|
|
23
|
+
`no-restricted-imports`, `pins/host-boundary.test.ts`, `pins/sdk-confinement.test.ts`). The
|
|
24
|
+
`./protocol` subpath cannot reach `src/host/`, so a consumer of the wire types acquires nothing
|
|
25
|
+
that can spawn a process.
|
|
26
|
+
|
|
27
|
+
Where a concept exists in the Agent Client Protocol, the name is ACP's (`session/new` style,
|
|
28
|
+
camelCase keys, snake_case discriminators). Periscope is not ACP-compatible and does not claim to
|
|
29
|
+
be; no ACP transport dials out. The names that are the package's own (`link`, `bulk`) are registered
|
|
30
|
+
in `HOST_NOUNS` (`src/core/vocab.ts`).
|
|
31
|
+
|
|
32
|
+
## Composition
|
|
33
|
+
|
|
34
|
+
Two things live in `src/host/host.ts`.
|
|
35
|
+
|
|
36
|
+
`composeSession(options)` assembles one session: machine, observer, gate, forwarding. It takes
|
|
37
|
+
everything as arguments, so it is checkable without a socket or a process, and returns
|
|
38
|
+
`{ sessionKey, session, machine, observer, detach }`.
|
|
39
|
+
|
|
40
|
+
- Forwarding is attached before the first transition is recorded. The forwarder subscribes to the
|
|
41
|
+
machine; attached later, the `spawning` record goes to nobody, and it is the first frame a
|
|
42
|
+
controller sees for a session.
|
|
43
|
+
- Observation hooks are registered before the gate's:
|
|
44
|
+
`mergeHooks(observationHooks(...), permissionHooks(...))`. Matchers dispatch in array order but
|
|
45
|
+
are awaited concurrently, and a gate registered first would open a hold the observer closes in
|
|
46
|
+
the same event. No pin enforces the order; it is stated at both sites.
|
|
47
|
+
- Every gate outcome is recorded on the machine unconditionally. `onOutcome` is an extra listener,
|
|
48
|
+
never a substitute.
|
|
49
|
+
|
|
50
|
+
`PeriscopeHost` owns the link and the registry and turns inbound payloads into calls on them.
|
|
51
|
+
|
|
52
|
+
- It answers exactly the payload kinds its dispatch names (the session commands, `bulk_request` and
|
|
53
|
+
the host-scoped asks in [protocol.md](protocol.md)) and refuses everything else
|
|
54
|
+
`frame-malformed`. A command that vanished would read as a host that hung.
|
|
55
|
+
- Two `session_new` for one handle never replace each other silently: the duplicate guard is a
|
|
56
|
+
synchronous reservation that holds across the workspace provider's await.
|
|
57
|
+
- A workspace claimed and then unusable is handed back on every failure path; provision and release
|
|
58
|
+
stay balanced.
|
|
59
|
+
- Shutdown ends sessions first, then the link, so the end transitions have somewhere to go.
|
|
60
|
+
`forgetSession` does not discard unacknowledged frames.
|
|
61
|
+
- `baseEnv` and `homeDir` build the agent's environment. Passing them beside a `registry` is refused
|
|
62
|
+
at construction: the host reads them only to build its own registry, and the pair would otherwise
|
|
63
|
+
be ignored in silence.
|
|
64
|
+
- `linkTimings` (`heartbeatIntervalMs`, `heartbeatTimeoutMs`, `connectTimeoutMs`) and `backoff`
|
|
65
|
+
reach the default link; `link` replaces the link wholesale.
|
|
66
|
+
|
|
67
|
+
A session refused at its open answers on the wire ([protocol.md](protocol.md), the refused open).
|
|
68
|
+
|
|
69
|
+
## The binary
|
|
70
|
+
|
|
71
|
+
`periscope` is six verbs (`src/bin/command.ts`): `serve` (the default: no arguments means `serve`),
|
|
72
|
+
`login`, `pair <code>`, `config`, `status` and `help`. An unrecognised first argument is `unknown`,
|
|
73
|
+
never `serve`; falling back would let a typo start a host. Every claim that the binary does not do
|
|
74
|
+
something is about the daemon; the other verbs are where interactive work lives.
|
|
75
|
+
|
|
76
|
+
- `serve` dials the controller and serves sessions. It refuses to start as root (an unattended
|
|
77
|
+
agent as root has the whole machine on every tool call; refused by policy, before anything else is
|
|
78
|
+
read) and refuses to start without `PERISCOPE_DECISION_URL` (a host that cannot ask is an open door or a
|
|
79
|
+
session where nothing runs). It never signs anyone in: it presents credentials that are already
|
|
80
|
+
there and refuses by name when they are not. It prints one `[host]` posture line before dialling
|
|
81
|
+
and writes the link's state to `link-state.json` beside the credentials on every transition.
|
|
82
|
+
- `login` runs the sign-in and writes the token cache. `pair <code>`, with `--controller <origin>`
|
|
83
|
+
and `--label <name>`, redeems a controller-minted code for a paired credential and writes the
|
|
84
|
+
controller's addresses to the config file. `config` shows or edits `config.json`. `status` prints
|
|
85
|
+
the whole posture from the link record, the credentials and the settings, and never dials.
|
|
86
|
+
- `config.json` lives beside the credentials under a closed key allowlist (`CONFIG_KEYS`: the two
|
|
87
|
+
control-plane URLs, the host id, both roots, the branch scheme, the workspace key, the agent home;
|
|
88
|
+
`PERISCOPE_CONFIG_DIR` is excluded, a file cannot move itself). `serve`, `login`, `pair` and
|
|
89
|
+
`status` read the environment with the file filling its absences; the environment always wins. A
|
|
90
|
+
corrupt file is fatal by name for all four. `config` itself reads the raw environment so it can
|
|
91
|
+
still name a broken file.
|
|
92
|
+
- The agent home is the setting (`PERISCOPE_AGENT_HOME`, default `<home>/.claude`); the transcripts
|
|
93
|
+
root derives from it.
|
|
94
|
+
|
|
95
|
+
## Sessions
|
|
96
|
+
|
|
97
|
+
`SessionRegistry` owns session lifetime as instance state, so two embedders in one process cannot
|
|
98
|
+
see each other's sessions; everyone else borrows a `HostedSession` handle.
|
|
99
|
+
|
|
100
|
+
- The agent emits nothing until a turn is queued. `create()` is synchronous provisioning with no id;
|
|
101
|
+
`open(request & { prompt })` is the ordinary one-call shape. Waiting for output before prompting
|
|
102
|
+
hangs with no diagnostic.
|
|
103
|
+
- The spawn environment is a derived allow-list, never inherited (`src/sessions/spawn-env.ts`):
|
|
104
|
+
exact, prefix and suffix rules, case-insensitive, deny before allow, and the host-session strip
|
|
105
|
+
(`HOST_SESSION_MARKERS`) runs last, after `extraEnv`, so the recorded hazards cannot be
|
|
106
|
+
re-admitted. Product-specific keys come in through `extraAllowedKeys` and `extraEnv`.
|
|
107
|
+
- Workspace trust is read, never granted (`src/host/workspace-trust.ts`). The package default is
|
|
108
|
+
`settingSources: []`, the only setting under which the host can state what an agent's permissions
|
|
109
|
+
are; what it costs is `CLAUDE.md` loading. The project tier loads only in a directory the CLI
|
|
110
|
+
trusts, which a provisioned worktree is not.
|
|
111
|
+
- One consumer of the SDK stream: `HostedSession` pumps it once and fans out on `onMessage`,
|
|
112
|
+
`onEnd` and `onDegrade`. Subscribe; never iterate the stream yourself. A throwing subscriber
|
|
113
|
+
becomes a `subscriber_failed` degrade and cannot kill the pump.
|
|
114
|
+
- The CLI version is a per-spawn fact on `system/init`. `apiKeySource` is provenance, never an
|
|
115
|
+
"is this authenticated" predicate: it reads `none` on a session that billed.
|
|
116
|
+
|
|
117
|
+
A `session_prompt` arriving while its session is still opening is held, not refused. `session_new`
|
|
118
|
+
awaits the workspace provider, and a git worktree takes seconds; the host acknowledges no controller
|
|
119
|
+
frame, so the sender cannot know the open finished. A held turn is delivered when the session
|
|
120
|
+
registers, refused in the open's `finally` on every failure path, or withdrawn by a `session_cancel`
|
|
121
|
+
during the open; it is never dropped. The hold is bounded at `MAX_HELD_TURNS`; overflow refuses
|
|
122
|
+
`session-unknown` with the bound in its detail. A cancel during the open withdraws the turn and does
|
|
123
|
+
not stop the open. `HostEvent` reports `prompt-held`, `prompt-delivered` and `prompt-withdrawn`.
|
|
124
|
+
|
|
125
|
+
## The two read-only doors
|
|
126
|
+
|
|
127
|
+
`src/host/claude-transcripts.ts` enumerates and tails the agent CLI's own transcript directory
|
|
128
|
+
(`<agentHome>/projects`), so a controller can list and open sessions the machine ran outside this
|
|
129
|
+
host. `src/host/paths.ts` names the same directory, the configured agent home included, to
|
|
130
|
+
protect it from the sessions this host runs;
|
|
131
|
+
the two postures do not touch, and nothing here can write. Read-only is a property: the modules'
|
|
132
|
+
whole filesystem surface is `createReadStream`, `readdir`, `stat` and `realpath`, held by
|
|
133
|
+
`pins/transcript-readonly.test.ts`.
|
|
134
|
+
|
|
135
|
+
- Every caller-supplied name passes three layers: a strict allowlist, an explicit `.` and `..`
|
|
136
|
+
reject, and resolve-then-containment. A violation refuses `transcript-path-escape` naming the
|
|
137
|
+
layer. An absent transcript is a value, never a refusal.
|
|
138
|
+
- Project slugs are opaque; the CLI's flattening of a path is not invertible and nothing here tries.
|
|
139
|
+
- The CLI rewrites a transcript on compaction, so every answer carrying an offset also carries the
|
|
140
|
+
file's size and mtime; a caller seeing either move re-reads whole.
|
|
141
|
+
- Each listed transcript carries the `cwd` the CLI recorded, because the CLI keeps transcripts per
|
|
142
|
+
directory: a resume must run there or it becomes a fresh session that says nothing.
|
|
143
|
+
|
|
144
|
+
`src/host/repository-read.ts` reads the repository under `PERISCOPE_REPOSITORY_ROOT`:
|
|
145
|
+
`repository_list` (names, sorted, capped at `MAX_REPOSITORY_ENTRIES`) and `repository_read` (a text
|
|
146
|
+
head capped at `MAX_REPOSITORY_READ_BYTES`, cut on a character boundary, with the whole size). The
|
|
147
|
+
jail is lexical (join, resolve, contain) and physical (`realpath` both sides, contain again); a file
|
|
148
|
+
whose head holds a NUL byte refuses `repository-read-failed` rather than shipping binary as text.
|
|
149
|
+
The listing does not follow links; the read does.
|
|
150
|
+
|
|
151
|
+
## Workspaces
|
|
152
|
+
|
|
153
|
+
`WorkspaceProvider` is the seam the SDK does not have: what a workspace is. Two implementations
|
|
154
|
+
ship, `PlainDirProvider` and `GitWorktreeProvider`; policies (a shared directory per piece of work, a
|
|
155
|
+
cleanup schedule) belong to the embedder. Every result is a named refusal, never an exception.
|
|
156
|
+
|
|
157
|
+
- The binary selects the git provider on the presence of `PERISCOPE_REPOSITORY_ROOT`
|
|
158
|
+
(`src/bin/workspaces.ts`). `PERISCOPE_WORKSPACE_ROOT` alone gives plain directories. Neither gives
|
|
159
|
+
no provider and the controller's `cwd` verbatim, and a directory nobody created fails the spawn.
|
|
160
|
+
- The workspace key resolves `session_new.workspaceKey ?? PERISCOPE_WORKSPACE_KEY ?? sessionKey`,
|
|
161
|
+
each a default for an absence. It is screened before any claim (`src/core/workspace-id.ts`: the
|
|
162
|
+
union of git refname law and path-segment law, plus `MAX_WORKSPACE_ID_LENGTH`; the refusal names
|
|
163
|
+
the character and which law rejects it), and a non-null key with no provider refuses
|
|
164
|
+
`workspace-provision-failed`. Sessions can share a key; the in-use guard holds until the last
|
|
165
|
+
closes, inside the per-key turn (`src/core/keyed-turns.ts` serialises provision, release and
|
|
166
|
+
close per key, and different keys interleave freely).
|
|
167
|
+
- The branch scheme is the caller's template: `PERISCOPE_BRANCH_SCHEME` with `{key}` and `{repo}`,
|
|
168
|
+
default `{repo}/{key}`. The posture is screened at start-up, fatally, by name: an unknown
|
|
169
|
+
placeholder, an illegal literal, a brace that survives substitution, a dependent setting absent.
|
|
170
|
+
- `inventory()` lists the worktrees contained by the workspace root (never the operator's checkout),
|
|
171
|
+
newest tip first, with branch, head, merged (git's answer: a squash merge reads unmerged),
|
|
172
|
+
`aheadCount` and `lastCommitAt`.
|
|
173
|
+
- `release(key, { remove, deleteBranch, force })`: `remove` alone leaves the branch; `deleteBranch`
|
|
174
|
+
deletes a merged branch; an unmerged branch refuses `branch-not-merged` and removes nothing unless
|
|
175
|
+
`force`. The receipt states `directoryRemoved` and `branchDeleted` separately. A session ending
|
|
176
|
+
leaves its directory; the reap is the one path that removes one.
|
|
177
|
+
- `host_configure` reconfigures the running host (`src/bin/reconfigure.ts`): the two roots, the
|
|
178
|
+
branch scheme, the agent home and the two control-plane URLs. A root change while a session is
|
|
179
|
+
open or opening refuses `config-host-busy`. The URLs are written but apply at the next start, and
|
|
180
|
+
are named in `pendingRestart` on every hello and answer until then.
|
|
181
|
+
- Under a provider the controller's `cwd` is advisory; the `spawning` transition carries the
|
|
182
|
+
directory the session got. One exception: a `cwd` equal to the provider's own `repositoryRoot`
|
|
183
|
+
runs there, the operator's trusted checkout, claiming no key. A resume runs where its transcript
|
|
184
|
+
lives or refuses `resume-cwd-not-honoured` on the wire.
|
|
185
|
+
|
|
186
|
+
## MCP
|
|
187
|
+
|
|
188
|
+
The host's own MCP server registers tool descriptors the embedder supplies: JSON in, Zod inside
|
|
189
|
+
(`src/mcp/schema.ts`). An unconvertible construct is refused at registration, never widened to
|
|
190
|
+
`z.any()`, never skipped. No payload kind carries a descriptor. A controller declares MCP servers
|
|
191
|
+
through `session_new.request.mcpServers` (a name colliding with a host-registered server is
|
|
192
|
+
refused), and every tool they add is decided by the same gate as `Bash`. `identity` is filled by
|
|
193
|
+
the host at call time.
|
|
194
|
+
|
|
195
|
+
## Identity
|
|
196
|
+
|
|
197
|
+
The host signs in a real user through a generic OIDC client and presents that user's token; a
|
|
198
|
+
paired machine credential is the alternative for a host meant to stay up.
|
|
199
|
+
|
|
200
|
+
- The authority and client id are configuration (`PERISCOPE_IDENTITY_AUTHORITY`,
|
|
201
|
+
`PERISCOPE_IDENTITY_CLIENT_ID`), never a constant; absence is a named refusal at start-up.
|
|
202
|
+
Loopback with PKCE is the primary flow; the device-code flow is opt-in. `offline_access` is in
|
|
203
|
+
the default scope set so an unattended host can refresh.
|
|
204
|
+
- `readCredential` prefers the paired credential (`p1.<hostId>.<secret>`, beside the token cache)
|
|
205
|
+
over the token cache even when both exist: a refresh token dies after inactivity and a paired
|
|
206
|
+
credential has no clock. A missing paired file falls through; a corrupt one is fatal, never a
|
|
207
|
+
fallback. The paired credential's host id outranks `PERISCOPE_HOST_ID` at the hello.
|
|
208
|
+
- A controller that refuses the credential at the door (401 or 403) is `link-unauthorized`, and it
|
|
209
|
+
is terminal: the process exits 1 naming it, because redialling a door that refused the identity
|
|
210
|
+
is a loop with no exit.
|
|
211
|
+
- The token cache and the paired credential are protected by derivation: `credentialPaths(env)` is
|
|
212
|
+
the one source of the gate's protected set and the cache's own location.
|
|
213
|
+
- File-mode privacy is measured, never assumed from the platform: `enforced`, `write-bit-only`
|
|
214
|
+
(the Windows shape) or `unobservable`. A mode that is not enforced is said by name as a degrade.
|
|
215
|
+
- `signIn` and `signInWithDeviceCode` are exported for an embedder; the daemon does not call them.
|
|
216
|
+
|
|
217
|
+
## Persistence and telemetry
|
|
218
|
+
|
|
219
|
+
- A delivery receipt is read from raw stored entries, never from the SDK's conversation reader:
|
|
220
|
+
compaction relinks `parentUuid`, so a walk cannot reach pre-compaction turns. Three defences hold
|
|
221
|
+
the receipt: raw entries, a uuid anchor (never a numeric offset), and compaction-produced entries
|
|
222
|
+
excluded as candidates.
|
|
223
|
+
- The transition log is append-only, and that is load-bearing ([state-machine.md](state-machine.md)).
|
|
224
|
+
- Spend is consumed as the agent reports it, per model. Nothing multiplies tokens by a rate.
|
|
225
|
+
|
|
226
|
+
## What ships
|
|
227
|
+
|
|
228
|
+
- Exports are exactly `.` and `./protocol`; a third subpath would reopen the boundary
|
|
229
|
+
(`pins/package-shape.test.ts`, `pins/protocol-closure.test.ts`).
|
|
230
|
+
- `files` is an allowlist: `dist/` without tests, maps, pins and test support, plus README,
|
|
231
|
+
SECURITY and LICENSE. `npm pack` is gated against it.
|
|
232
|
+
- `contracts/` holds the approved snapshots, in the repository and not the tarball: `wire-vectors/`
|
|
233
|
+
(every payload kind byte for byte, the refusal and tolerance cases), `public-api.txt` (every
|
|
234
|
+
export of both barrels with its signature), and the installed SDK's type hash with its version.
|
|
235
|
+
`npm run contracts:update` re-approves the first two after an intended change;
|
|
236
|
+
`npm run check:drift` compares the SDK.
|
|
237
|
+
- The suite is `npm test`: a clean build, then every `*.test.js`. Its figure is four numbers per
|
|
238
|
+
platform (tests, pass, fail, skipped) with the invocation. Live probes skip loudly unless
|
|
239
|
+
`PERISCOPE_LIVE=1`.
|
|
240
|
+
|
|
241
|
+
## How not to use it
|
|
242
|
+
|
|
243
|
+
- Do not compose by hand and leave `grantOnAllow` off: the gate can refuse and cannot approve, and
|
|
244
|
+
every allowed call silently fails to happen. Use `PeriscopeHost`, or set the flag and pass
|
|
245
|
+
`onRefusal` and `onDegrade`.
|
|
246
|
+
- Do not assume the daemon signs anyone in. `login` writes the token cache; `pair` writes the
|
|
247
|
+
paired credential.
|
|
248
|
+
- Do not assume a host's id is what its config says; the paired credential's id wins.
|
|
249
|
+
- Do not point a bulk `postUrl` outside the controller's own origin; it refuses
|
|
250
|
+
`bulk-target-not-controller`.
|
|
251
|
+
- Do not expect one transport. A controller serves the WebSocket and an HTTP decision endpoint.
|
|
252
|
+
- Do not add an inbound port, a payload lane or a second stream consumer. Dial-out, commands only
|
|
253
|
+
and a single pump are the design.
|
|
254
|
+
- Do not teach it a controller's vocabulary, a roster, or an interpretation of `correlationId`.
|
|
255
|
+
- Do not make retransmission skip deltas, and do not put deltas in a durable store; both halves are
|
|
256
|
+
pinned.
|
|
257
|
+
- One controller and one replica per host.
|
|
258
|
+
|
|
259
|
+
## Where things live
|
|
260
|
+
|
|
261
|
+
| Path | What |
|
|
262
|
+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| `src/index.ts` | The public barrel; anything not exported here or from `protocol.ts` is internal |
|
|
264
|
+
| `src/protocol.ts` | The `./protocol` subpath: types and runtime values whose closure reaches no `host/` file and no `node:` builtin |
|
|
265
|
+
| `src/bin/` | The binary: `command.ts` (the verbs), `main.ts` (dispatch through `Io`), `serve.ts` (the daemon), `login.ts`, `pair.ts`, `config.ts`, `status.ts`, `posture.ts`, `workspaces.ts` (the provider selector and the scheme), `reconfigure.ts` |
|
|
266
|
+
| `src/host/` | The only importer of the SDK and the process-reaching builtins: `host.ts`, `agent-process.ts` (the SDK seam), `hooks.ts`, `paths.ts`, `config-file.ts`, `link-state-file.ts`, `claude-transcripts.ts`, `repository-read.ts`, `bulk-post.ts`, `wire-request.ts`, `sign-in.ts` |
|
|
267
|
+
| `src/control/` | The wire: `frames.ts`, `codec.ts`, `seq.ts`, `queue.ts`, `link.ts`, `link-state.ts`, `stream.ts`, `stream-routing.ts` |
|
|
268
|
+
| `src/sessions/` | `registry.ts`, `session.ts`, `spawn-env.ts` |
|
|
269
|
+
| `src/state/` | `model.ts`, `machine.ts`, `observer.ts`, `store.ts`, `coverage.ts`, `reporter.ts` |
|
|
270
|
+
| `src/gate/` | `gate.ts`, `decision.ts`, `outcome.ts`, `local.ts`, `jail.ts`, `shell.ts`, `command.ts`, `escalate.ts` |
|
|
271
|
+
| `src/workspace/` | `provider.ts`, `plain-dir.ts`, `git-worktree.ts`, `worktree-porcelain.ts` |
|
|
272
|
+
| `src/identity/` | Generic OIDC: config, PKCE, authorize, device code, the token store, the file-mode measurement |
|
|
273
|
+
| `src/mcp/` | JSON Schema to Zod, the descriptor, the server |
|
|
274
|
+
| `src/persistence/`, `src/telemetry/` | The receipt, the transition log, per-model spend |
|
|
275
|
+
| `src/pins/` | The structural tests; `walk.ts` is the tree walker they share |
|
|
276
|
+
| `examples/` | The minimal controller, the reference controller with its runnable entry, and the parallel-run proof |
|
|
277
|
+
| `contracts/` | The approved snapshots |
|