@zgeoff/atc 3.3.1 → 3.4.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/package.json +4 -2
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +11 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +13 -1
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +48 -15
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +85 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway.ts +4 -2
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +15 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +22 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- package/src/test-utils/build-stub-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/{fixture-imp-port.ts → build-stub-imp-port.ts} +119 -63
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -0
- package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +120 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +5 -5
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +99 -0
- package/src/test-utils/setup-mcp-http.ts +19 -31
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +25 -8
- package/src/test-utils/start-daemon-process.ts +192 -0
- package/src/test-utils/start-git-http-server.ts +21 -3
- package/src/test-utils/start-mcp-stdio.ts +195 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +103 -0
- package/src/test-utils/start-stub-impd-info.ts +32 -0
- package/src/test-utils/start-stub-impd.ts +138 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +166 -0
- package/src/test-utils/start-stub-mcp-server.ts +50 -0
- package/src/test-utils/start-stub-refusing-listener.ts +38 -0
- package/src/test-utils/start-stub-reporter-socket.ts +77 -0
- package/src/test-utils/start-stub-restore-daemon.ts +58 -0
- package/src/test-utils/start-stub-session-bridge.ts +98 -0
- package/src/test-utils/start-stub-silent-server.ts +33 -0
- package/src/test-utils/start-stub-source-daemon.ts +39 -0
- package/src/test-utils/start-stub-stalled-listener.ts +40 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +70 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +26 -0
- package/src/test-utils/start-test-daemon.ts +215 -0
- package/src/test-utils/start-tui-harness.ts +284 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +59 -12
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- package/src/test-utils/start-legacy-daemon.ts +0 -111
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExecutionProvider, HarnessSpec, HostRequest } from '../daemon/execution-provider';
|
|
2
|
+
import { LocalPTYProvider } from '../daemon/local-pty-provider';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An execution provider that runs on this machine as the local
|
|
6
|
+
* pseudo-terminal provider does, except that each host preparation holds
|
|
7
|
+
* until the test calls `release` with that host, so a test can act while a
|
|
8
|
+
* harness is still starting. `prepares` holds the host of each preparation
|
|
9
|
+
* in the order it began, and `harnesses` each harness spec in the order it
|
|
10
|
+
* started.
|
|
11
|
+
*/
|
|
12
|
+
export function buildStubHeldProvider() {
|
|
13
|
+
const local = new LocalPTYProvider();
|
|
14
|
+
const gates = new Map<string, PromiseWithResolvers<void>>();
|
|
15
|
+
|
|
16
|
+
const prepares: string[] = [];
|
|
17
|
+
const harnesses: HarnessSpec[] = [];
|
|
18
|
+
|
|
19
|
+
const getGate = (host: string): PromiseWithResolvers<void> => {
|
|
20
|
+
const gate = gates.get(host) ?? Promise.withResolvers<void>();
|
|
21
|
+
|
|
22
|
+
gates.set(host, gate);
|
|
23
|
+
|
|
24
|
+
return gate;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const provider: ExecutionProvider = {
|
|
28
|
+
kind: local.kind,
|
|
29
|
+
remote: local.remote,
|
|
30
|
+
capabilities: local.capabilities,
|
|
31
|
+
prepareHost: async (request: HostRequest) => {
|
|
32
|
+
prepares.push(request.host);
|
|
33
|
+
|
|
34
|
+
await getGate(request.host).promise;
|
|
35
|
+
|
|
36
|
+
await local.prepareHost();
|
|
37
|
+
},
|
|
38
|
+
spawnHarness: (spec) => {
|
|
39
|
+
harnesses.push(spec);
|
|
40
|
+
|
|
41
|
+
return local.spawnHarness(spec);
|
|
42
|
+
},
|
|
43
|
+
transferArchive: local.transferArchive,
|
|
44
|
+
runCommand: local.runCommand,
|
|
45
|
+
suspendHost: local.suspendHost,
|
|
46
|
+
destroyHost: local.destroyHost,
|
|
47
|
+
dispose: local.dispose,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
provider,
|
|
52
|
+
prepares,
|
|
53
|
+
harnesses,
|
|
54
|
+
release: (host: string) => {
|
|
55
|
+
getGate(host).resolve();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The script of a stand-in `zoxide` that lists no directories, and waits
|
|
3
|
+
* for the test first: while `$HOME/zoxide-hold` exists, it touches
|
|
4
|
+
* `$HOME/zoxide-held` and waits for the file to go.
|
|
5
|
+
*/
|
|
6
|
+
export function buildStubHeldZoxide(): string {
|
|
7
|
+
return `#!/bin/sh
|
|
8
|
+
if [ -f "$HOME/zoxide-hold" ]; then
|
|
9
|
+
touch "$HOME/zoxide-held"
|
|
10
|
+
while [ -f "$HOME/zoxide-hold" ]; do sleep 0.05; done
|
|
11
|
+
fi
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host operation a test can hold open, for a host preparation or sleep
|
|
3
|
+
* that must stay in flight while the test changes something. Unarmed, the
|
|
4
|
+
* operation resolves at once, so the arrange steps before the hold pass
|
|
5
|
+
* through. Once armed, the next call resolves `entered` and every call
|
|
6
|
+
* waits until `release`; after the release, calls resolve at once again.
|
|
7
|
+
*/
|
|
8
|
+
export function buildStubHostHold() {
|
|
9
|
+
const entered = Promise.withResolvers<void>();
|
|
10
|
+
const released = Promise.withResolvers<void>();
|
|
11
|
+
let armed = false;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
hold: (): Promise<void> => {
|
|
15
|
+
if (!armed) {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
entered.resolve();
|
|
20
|
+
|
|
21
|
+
return released.promise;
|
|
22
|
+
},
|
|
23
|
+
arm: () => {
|
|
24
|
+
armed = true;
|
|
25
|
+
},
|
|
26
|
+
entered: entered.promise,
|
|
27
|
+
release: () => {
|
|
28
|
+
released.resolve();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExecutionProvider } from '../daemon/execution-provider';
|
|
2
|
+
import { LocalPTYProvider } from '../daemon/local-pty-provider';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An execution provider of kind `imp-like` whose hosts can be put to sleep
|
|
6
|
+
* and destroyed, as an imp's can, while every harness, archive transfer, and
|
|
7
|
+
* command runs on this machine through the local terminal provider.
|
|
8
|
+
* Readying, suspending, and destroying a host each do nothing and settle at
|
|
9
|
+
* once.
|
|
10
|
+
*/
|
|
11
|
+
export function buildStubHostProvider(): ExecutionProvider {
|
|
12
|
+
const local = new LocalPTYProvider();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
kind: 'imp-like',
|
|
16
|
+
remote: false,
|
|
17
|
+
capabilities: { ...local.capabilities, suspend: true, destroy: true },
|
|
18
|
+
prepareHost: () => Promise.resolve(),
|
|
19
|
+
spawnHarness: local.spawnHarness,
|
|
20
|
+
transferArchive: local.transferArchive,
|
|
21
|
+
runCommand: local.runCommand,
|
|
22
|
+
suspendHost: () => Promise.resolve(),
|
|
23
|
+
destroyHost: () => Promise.resolve(),
|
|
24
|
+
dispose: local.dispose,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -36,39 +36,24 @@ const RING_BYTES = 262_144;
|
|
|
36
36
|
// The mode bytes a fresh attach sends ahead of a ring that has wrapped.
|
|
37
37
|
const PRELUDE = '\u001B[0m';
|
|
38
38
|
|
|
39
|
-
// The PATH every
|
|
39
|
+
// The PATH every stub process runs with: a guest's own, never the
|
|
40
40
|
// daemon's.
|
|
41
41
|
const GUEST_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* An in-process stand-in for impd behind the imp port. Each imp is a set of
|
|
45
|
-
* real `bun-pty` processes on this machine; a sleeping imp stops them with
|
|
46
|
-
* SIGSTOP and a wake continues them, so a memory wake keeps each process
|
|
47
|
-
* and its generation. Every session keeps an exact 262144-byte ring with
|
|
48
|
-
* offsets, a fresh attach skips to the next line and sends a mode prelude,
|
|
49
|
-
* and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
|
|
50
|
-
* `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
|
|
51
|
-
* and `FORBIDDEN`. Leases belong to principals; the port acts as
|
|
52
|
-
* `principal`, and a test adds other owners' leases, cold boots, and
|
|
53
|
-
* dropped sockets through the controls. Grants follow impd 0.27: the
|
|
54
|
-
* caller's identity must reach the imp and, under imp patterns, list the
|
|
55
|
-
* secret as grantable; a grant is idempotent, one secret per host, and a
|
|
56
|
-
* destroyed imp or a rebound or removed secret takes its grants with it. A
|
|
57
|
-
* start or an attach that requires the broker is refused with
|
|
58
|
-
* `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
|
|
59
|
-
* while the broker fails or the imp holds no grant, when the start sets a
|
|
60
|
-
* broker variable, and when it would join a process that started without
|
|
61
|
-
* the broker required.
|
|
62
|
-
*/
|
|
63
43
|
// A hold on the commands whose argv holds its text: entered resolves with
|
|
64
44
|
// the argv of the first command it holds, and stop lets every command it
|
|
65
|
-
// holds run.
|
|
66
|
-
|
|
45
|
+
// holds run. Disposing the hold stops it, so a test that holds it with
|
|
46
|
+
// `using` after its daemon lets the daemon stop even when the test fails.
|
|
47
|
+
interface StubCommandHold {
|
|
67
48
|
readonly entered: Promise<string>;
|
|
68
49
|
readonly stop: () => void;
|
|
50
|
+
readonly [Symbol.dispose]: () => void;
|
|
69
51
|
}
|
|
70
52
|
|
|
71
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The impd stand-in the factory below builds, one per call.
|
|
55
|
+
*/
|
|
56
|
+
class StubImpPort implements ImpPort {
|
|
72
57
|
// Every port call, in order, as `<call> <imp> [<detail>]`.
|
|
73
58
|
readonly calls: string[] = [];
|
|
74
59
|
|
|
@@ -150,6 +135,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
150
135
|
// The lease acquisitions wait for this hold to end, while one is held.
|
|
151
136
|
private leaseHold: PromiseWithResolvers<void> | null = null;
|
|
152
137
|
|
|
138
|
+
// How many lease acquisitions and releases wait on their holds.
|
|
139
|
+
private heldLeases = 0;
|
|
140
|
+
|
|
141
|
+
private heldReleases = 0;
|
|
142
|
+
|
|
153
143
|
// The directory a relative or missing working directory resolves
|
|
154
144
|
// against inside every imp, as a guest's home does; null for the test's
|
|
155
145
|
// own working directory.
|
|
@@ -183,9 +173,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
183
173
|
// what the daemon writes still reaches the guest.
|
|
184
174
|
private droppingGuestBytes = false;
|
|
185
175
|
|
|
176
|
+
// How many guest bytes the drop has thrown away.
|
|
177
|
+
private droppedGuestBytes = 0;
|
|
178
|
+
|
|
186
179
|
private readonly principal: string;
|
|
187
180
|
|
|
188
|
-
private readonly imps = new Map<string,
|
|
181
|
+
private readonly imps = new Map<string, StubImp>();
|
|
189
182
|
|
|
190
183
|
private readonly forwards = new Set<{ stop: () => void; stopRelays: () => void }>();
|
|
191
184
|
|
|
@@ -308,7 +301,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
308
301
|
);
|
|
309
302
|
}
|
|
310
303
|
|
|
311
|
-
const imp:
|
|
304
|
+
const imp: StubImp = {
|
|
312
305
|
id: randomUUID(),
|
|
313
306
|
name: spec.name,
|
|
314
307
|
state: 'stopped',
|
|
@@ -335,8 +328,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
335
328
|
}
|
|
336
329
|
|
|
337
330
|
private async waitForLease(name: string, label: string, ttlSeconds: number): Promise<ImpLease> {
|
|
331
|
+
this.heldLeases += 1;
|
|
332
|
+
|
|
338
333
|
await this.leaseHold?.promise;
|
|
339
334
|
|
|
335
|
+
this.heldLeases -= 1;
|
|
336
|
+
|
|
340
337
|
return this.applyLease(name, label, ttlSeconds);
|
|
341
338
|
}
|
|
342
339
|
|
|
@@ -401,8 +398,12 @@ export class FixtureImpPort implements ImpPort {
|
|
|
401
398
|
}
|
|
402
399
|
|
|
403
400
|
private async waitForRelease(name: string, label: string): Promise<boolean> {
|
|
401
|
+
this.heldReleases += 1;
|
|
402
|
+
|
|
404
403
|
await this.releaseHold?.promise;
|
|
405
404
|
|
|
405
|
+
this.heldReleases -= 1;
|
|
406
|
+
|
|
406
407
|
return this.applyRelease(name, label);
|
|
407
408
|
}
|
|
408
409
|
|
|
@@ -486,7 +487,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
486
487
|
|
|
487
488
|
const outcome = Promise.withResolvers<ImpSessionOutcome>();
|
|
488
489
|
|
|
489
|
-
const connection:
|
|
490
|
+
const connection: StubConnection = {
|
|
490
491
|
handlers,
|
|
491
492
|
sent: 0,
|
|
492
493
|
finished: false,
|
|
@@ -625,11 +626,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
625
626
|
): ImpReverseForward {
|
|
626
627
|
this.calls.push(`reverse ${name} ${guestPath}`);
|
|
627
628
|
|
|
628
|
-
const server = Bun.listen<
|
|
629
|
+
const server = Bun.listen<StubRelay>({
|
|
629
630
|
unix: guestPath,
|
|
630
631
|
socket: {
|
|
631
632
|
open: (socket) => {
|
|
632
|
-
const relay:
|
|
633
|
+
const relay: StubRelay = {
|
|
633
634
|
dataListeners: [],
|
|
634
635
|
closeListeners: [],
|
|
635
636
|
unsent: new Uint8Array(0),
|
|
@@ -675,6 +676,8 @@ export class FixtureImpPort implements ImpPort {
|
|
|
675
676
|
},
|
|
676
677
|
data: (socket, buf) => {
|
|
677
678
|
if (this.droppingGuestBytes) {
|
|
679
|
+
this.droppedGuestBytes += buf.length;
|
|
680
|
+
|
|
678
681
|
return;
|
|
679
682
|
}
|
|
680
683
|
|
|
@@ -884,6 +887,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
884
887
|
this.droppingGuestBytes = false;
|
|
885
888
|
}
|
|
886
889
|
|
|
890
|
+
// How many bytes guests wrote that the drop threw away.
|
|
891
|
+
countDroppedGuestBytes(): number {
|
|
892
|
+
return this.droppedGuestBytes;
|
|
893
|
+
}
|
|
894
|
+
|
|
887
895
|
/**
|
|
888
896
|
* Holds every session answer back, as a slow network does, until the
|
|
889
897
|
* hold stops.
|
|
@@ -892,6 +900,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
892
900
|
this.held ??= [];
|
|
893
901
|
}
|
|
894
902
|
|
|
903
|
+
// How many session answers wait for the answer hold to stop.
|
|
904
|
+
countHeldAnswers(): number {
|
|
905
|
+
return this.held?.length ?? 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
895
908
|
/**
|
|
896
909
|
* Sends every held answer, in order, and answers at once again.
|
|
897
910
|
*/
|
|
@@ -970,6 +983,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
970
983
|
this.leaseHold ??= Promise.withResolvers<void>();
|
|
971
984
|
}
|
|
972
985
|
|
|
986
|
+
// How many lease acquisitions wait for the lease hold to stop.
|
|
987
|
+
countHeldLeases(): number {
|
|
988
|
+
return this.heldLeases;
|
|
989
|
+
}
|
|
990
|
+
|
|
973
991
|
/**
|
|
974
992
|
* Lets every held lease acquisition go through, and the next at once.
|
|
975
993
|
*/
|
|
@@ -985,6 +1003,11 @@ export class FixtureImpPort implements ImpPort {
|
|
|
985
1003
|
this.releaseHold ??= Promise.withResolvers<void>();
|
|
986
1004
|
}
|
|
987
1005
|
|
|
1006
|
+
// How many lease releases wait for the release hold to stop.
|
|
1007
|
+
countHeldReleases(): number {
|
|
1008
|
+
return this.heldReleases;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
988
1011
|
/**
|
|
989
1012
|
* Lets every held lease release go through, and the next at once.
|
|
990
1013
|
*/
|
|
@@ -1007,7 +1030,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1007
1030
|
* stops. Throws while another hold is active, so no hold replaces one a
|
|
1008
1031
|
* command still waits on.
|
|
1009
1032
|
*/
|
|
1010
|
-
startCommandHold(match: string):
|
|
1033
|
+
startCommandHold(match: string): StubCommandHold {
|
|
1011
1034
|
if (this.commandHold !== null) {
|
|
1012
1035
|
throw new Error(`a hold on ${this.commandHold.match} is still active`);
|
|
1013
1036
|
}
|
|
@@ -1020,16 +1043,15 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1020
1043
|
|
|
1021
1044
|
this.commandHold = hold;
|
|
1022
1045
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.commandHold = null;
|
|
1028
|
-
}
|
|
1046
|
+
const stop = () => {
|
|
1047
|
+
if (this.commandHold === hold) {
|
|
1048
|
+
this.commandHold = null;
|
|
1049
|
+
}
|
|
1029
1050
|
|
|
1030
|
-
|
|
1031
|
-
},
|
|
1051
|
+
hold.done.resolve();
|
|
1032
1052
|
};
|
|
1053
|
+
|
|
1054
|
+
return { entered: hold.entered.promise, stop, [Symbol.dispose]: stop };
|
|
1033
1055
|
}
|
|
1034
1056
|
|
|
1035
1057
|
// Lets every command the active hold holds run, and the next at once.
|
|
@@ -1083,7 +1105,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1083
1105
|
return this.imps.get(name)?.state ?? null;
|
|
1084
1106
|
}
|
|
1085
1107
|
|
|
1086
|
-
// The names of the imps the
|
|
1108
|
+
// The names of the imps the stub holds.
|
|
1087
1109
|
collectImpNames(): string[] {
|
|
1088
1110
|
return [...this.imps.keys()];
|
|
1089
1111
|
}
|
|
@@ -1104,6 +1126,15 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1104
1126
|
return proc.generation;
|
|
1105
1127
|
}
|
|
1106
1128
|
|
|
1129
|
+
/**
|
|
1130
|
+
* The exit a session's process left that no connection has received, or
|
|
1131
|
+
* null while the process runs, once an attach delivered the exit, and for
|
|
1132
|
+
* a session the imp does not hold.
|
|
1133
|
+
*/
|
|
1134
|
+
findUndeliveredExit(name: string, session: string): { readonly code: number | null } | null {
|
|
1135
|
+
return this.imps.get(name)?.sessions.get(session)?.exited ?? null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1107
1138
|
// The offset after the last byte a session's running generation wrote.
|
|
1108
1139
|
getEnd(name: string, session: string): number {
|
|
1109
1140
|
const proc = this.getImp(name).sessions.get(session);
|
|
@@ -1174,7 +1205,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1174
1205
|
}
|
|
1175
1206
|
}
|
|
1176
1207
|
|
|
1177
|
-
private getImp(name: string):
|
|
1208
|
+
private getImp(name: string): StubImp {
|
|
1178
1209
|
const imp = this.imps.get(name);
|
|
1179
1210
|
|
|
1180
1211
|
if (imp === undefined) {
|
|
@@ -1184,7 +1215,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1184
1215
|
return imp;
|
|
1185
1216
|
}
|
|
1186
1217
|
|
|
1187
|
-
private buildView(imp:
|
|
1218
|
+
private buildView(imp: StubImp): ImpView {
|
|
1188
1219
|
const live = [...imp.leases.values()].filter((lease) => lease.until > Date.now());
|
|
1189
1220
|
const own = live.filter((lease) => lease.principal === this.principal);
|
|
1190
1221
|
const others = live.filter((lease) => lease.principal !== this.principal);
|
|
@@ -1200,7 +1231,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1200
1231
|
|
|
1201
1232
|
// A sleeping imp wakes from memory with every process as it was; a
|
|
1202
1233
|
// stopped one boots cold.
|
|
1203
|
-
private updateAwake(imp:
|
|
1234
|
+
private updateAwake(imp: StubImp): void {
|
|
1204
1235
|
if (imp.state === 'sleeping') {
|
|
1205
1236
|
imp.state = 'running';
|
|
1206
1237
|
|
|
@@ -1212,7 +1243,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1212
1243
|
}
|
|
1213
1244
|
}
|
|
1214
1245
|
|
|
1215
|
-
private updateAsleep(imp:
|
|
1246
|
+
private updateAsleep(imp: StubImp): void {
|
|
1216
1247
|
if (imp.state !== 'running') {
|
|
1217
1248
|
return;
|
|
1218
1249
|
}
|
|
@@ -1225,7 +1256,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1225
1256
|
}
|
|
1226
1257
|
}
|
|
1227
1258
|
|
|
1228
|
-
private bootCold(imp:
|
|
1259
|
+
private bootCold(imp: StubImp, cause: ColdBootCause): void {
|
|
1229
1260
|
for (const proc of imp.sessions.values()) {
|
|
1230
1261
|
proc.connection?.finish({ kind: 'detached', reason: 'lost', offset: proc.end });
|
|
1231
1262
|
proc.ended = true;
|
|
@@ -1246,7 +1277,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1246
1277
|
].slice(0, 4);
|
|
1247
1278
|
}
|
|
1248
1279
|
|
|
1249
|
-
private answerSession(request: ImpSessionRequest, connection:
|
|
1280
|
+
private answerSession(request: ImpSessionRequest, connection: StubConnection): void {
|
|
1250
1281
|
if (connection.finished) {
|
|
1251
1282
|
return;
|
|
1252
1283
|
}
|
|
@@ -1353,9 +1384,9 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1353
1384
|
// start sets, or a running process that started without the broker
|
|
1354
1385
|
// required, fails the requirement as impd's would.
|
|
1355
1386
|
private findBrokerProblem(
|
|
1356
|
-
imp:
|
|
1387
|
+
imp: StubImp,
|
|
1357
1388
|
request: ImpSessionRequest,
|
|
1358
|
-
running:
|
|
1389
|
+
running: StubProcess | undefined,
|
|
1359
1390
|
): string | null {
|
|
1360
1391
|
if (request.require?.includes('broker') !== true) {
|
|
1361
1392
|
return null;
|
|
@@ -1393,7 +1424,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1393
1424
|
return posix.resolve(this.homeDir, cwd ?? '.');
|
|
1394
1425
|
}
|
|
1395
1426
|
|
|
1396
|
-
private startProcess(imp:
|
|
1427
|
+
private startProcess(imp: StubImp, request: ImpSessionRequest): StubProcess | null {
|
|
1397
1428
|
if (request.kind !== 'start') {
|
|
1398
1429
|
return null;
|
|
1399
1430
|
}
|
|
@@ -1406,7 +1437,7 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1406
1437
|
env: { ...request.env },
|
|
1407
1438
|
});
|
|
1408
1439
|
|
|
1409
|
-
const proc:
|
|
1440
|
+
const proc: StubProcess = {
|
|
1410
1441
|
pty,
|
|
1411
1442
|
generation: randomBytes(16).toString('hex'),
|
|
1412
1443
|
ring: new Uint8Array(0),
|
|
@@ -1465,10 +1496,10 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1465
1496
|
}
|
|
1466
1497
|
|
|
1467
1498
|
private attachProcess(
|
|
1468
|
-
imp:
|
|
1469
|
-
proc:
|
|
1499
|
+
imp: StubImp,
|
|
1500
|
+
proc: StubProcess,
|
|
1470
1501
|
request: ImpSessionRequest,
|
|
1471
|
-
connection:
|
|
1502
|
+
connection: StubConnection,
|
|
1472
1503
|
created: boolean,
|
|
1473
1504
|
): void {
|
|
1474
1505
|
const bufferStart = proc.end - proc.ring.length;
|
|
@@ -1572,27 +1603,52 @@ export class FixtureImpPort implements ImpPort {
|
|
|
1572
1603
|
}
|
|
1573
1604
|
}
|
|
1574
1605
|
|
|
1575
|
-
|
|
1606
|
+
/**
|
|
1607
|
+
* Builds an in-process stand-in for impd behind the imp port, calling impd
|
|
1608
|
+
* as the principal, `token:atc` when absent. Each imp is a set of
|
|
1609
|
+
* real `bun-pty` processes on this machine; a sleeping imp stops them with
|
|
1610
|
+
* SIGSTOP and a wake continues them, so a memory wake keeps each process
|
|
1611
|
+
* and its generation. Every session keeps an exact 262144-byte ring with
|
|
1612
|
+
* offsets, a fresh attach skips to the next line and sends a mode prelude,
|
|
1613
|
+
* and impd's refusals carry its codes and data: `LEASED`, `LEASE_NOT_HELD`,
|
|
1614
|
+
* `NO_SESSION`, `INVALID_STATE`, `INVALID_RESUME`, `NOT_FOUND`, `CONFLICT`,
|
|
1615
|
+
* and `FORBIDDEN`. Leases belong to principals; the port acts as
|
|
1616
|
+
* `principal`, and a test adds other owners' leases, cold boots, and
|
|
1617
|
+
* dropped sockets through the controls. Grants follow impd 0.27: the
|
|
1618
|
+
* caller's identity must reach the imp and, under imp patterns, list the
|
|
1619
|
+
* secret as grantable; a grant is idempotent, one secret per host, and a
|
|
1620
|
+
* destroyed imp or a rebound or removed secret takes its grants with it. A
|
|
1621
|
+
* start or an attach that requires the broker is refused with
|
|
1622
|
+
* `PRECONDITION_FAILED` and reason `broker_not_ready`, and runs nothing,
|
|
1623
|
+
* while the broker fails or the imp holds no grant, when the start sets a
|
|
1624
|
+
* broker variable, and when it would join a process that started without
|
|
1625
|
+
* the broker required.
|
|
1626
|
+
*/
|
|
1627
|
+
export function buildStubImpPort(principal = 'token:atc'): StubImpPort {
|
|
1628
|
+
return new StubImpPort(principal);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
interface StubLease {
|
|
1576
1632
|
readonly principal: string;
|
|
1577
1633
|
readonly label: string;
|
|
1578
1634
|
readonly until: number;
|
|
1579
1635
|
}
|
|
1580
1636
|
|
|
1581
|
-
interface
|
|
1637
|
+
interface StubImp {
|
|
1582
1638
|
readonly id: string;
|
|
1583
1639
|
readonly name: string;
|
|
1584
1640
|
state: ImpState;
|
|
1585
1641
|
bootId: string;
|
|
1586
1642
|
coldBoots: ColdBoot[];
|
|
1587
|
-
readonly leases: Map<string,
|
|
1588
|
-
readonly sessions: Map<string,
|
|
1643
|
+
readonly leases: Map<string, StubLease>;
|
|
1644
|
+
readonly sessions: Map<string, StubProcess>;
|
|
1589
1645
|
readonly previous: Map<string, PreviousGeneration>;
|
|
1590
1646
|
|
|
1591
1647
|
// The secrets granted to the imp.
|
|
1592
1648
|
readonly grants: Set<string>;
|
|
1593
1649
|
}
|
|
1594
1650
|
|
|
1595
|
-
interface
|
|
1651
|
+
interface StubProcess {
|
|
1596
1652
|
readonly pty: IPty;
|
|
1597
1653
|
readonly generation: string;
|
|
1598
1654
|
ring: Uint8Array;
|
|
@@ -1601,22 +1657,22 @@ interface FixtureProcess {
|
|
|
1601
1657
|
|
|
1602
1658
|
// Ended by a cold boot: its exit is never delivered.
|
|
1603
1659
|
ended: boolean;
|
|
1604
|
-
connection:
|
|
1660
|
+
connection: StubConnection | null;
|
|
1605
1661
|
|
|
1606
1662
|
// Whether its start required the broker, which an attach that requires
|
|
1607
1663
|
// it needs.
|
|
1608
1664
|
readonly requireBroker: boolean;
|
|
1609
1665
|
}
|
|
1610
1666
|
|
|
1611
|
-
interface
|
|
1667
|
+
interface StubConnection {
|
|
1612
1668
|
readonly handlers: ImpSessionHandlers;
|
|
1613
1669
|
sent: number;
|
|
1614
1670
|
finished: boolean;
|
|
1615
|
-
process:
|
|
1671
|
+
process: StubProcess | null;
|
|
1616
1672
|
readonly finish: (outcome: ImpSessionOutcome) => void;
|
|
1617
1673
|
}
|
|
1618
1674
|
|
|
1619
|
-
interface
|
|
1675
|
+
interface StubRelay {
|
|
1620
1676
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
|
|
1621
1677
|
readonly dataListeners: ((data: Uint8Array) => void)[];
|
|
1622
1678
|
readonly closeListeners: (() => void)[];
|
|
@@ -1647,7 +1703,7 @@ function mergeTail(ring: Uint8Array, data: Uint8Array, limit: number): Uint8Arra
|
|
|
1647
1703
|
|
|
1648
1704
|
// Calls a connection's handler as the imp client does: a handler that
|
|
1649
1705
|
// throws ends the connection with a local error and closes it.
|
|
1650
|
-
function tryEmit(connection:
|
|
1706
|
+
function tryEmit(connection: StubConnection, emit: () => void): boolean {
|
|
1651
1707
|
try {
|
|
1652
1708
|
emit();
|
|
1653
1709
|
|
|
@@ -1662,7 +1718,7 @@ function tryEmit(connection: FixtureConnection, emit: () => void): boolean {
|
|
|
1662
1718
|
}
|
|
1663
1719
|
}
|
|
1664
1720
|
|
|
1665
|
-
function findSessionName(imp:
|
|
1721
|
+
function findSessionName(imp: StubImp, proc: StubProcess): string {
|
|
1666
1722
|
for (const [name, held] of imp.sessions) {
|
|
1667
1723
|
if (held === proc) {
|
|
1668
1724
|
return name;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A log sink that records each line it is given, in order, in place of the
|
|
3
|
+
* daemon's stderr log: `log` takes a line and `lines` holds every line taken
|
|
4
|
+
* so far.
|
|
5
|
+
*/
|
|
6
|
+
export function buildStubLog() {
|
|
7
|
+
const lines: string[] = [];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
lines,
|
|
11
|
+
log: (line: string): void => {
|
|
12
|
+
lines.push(line);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const CLI_PATH = join(import.meta.dir, '..', 'cli.ts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The script of a stand-in `claude` for the stdio MCP suites. It appends its
|
|
7
|
+
* pid to `$HOME/stub-pids`, prints `FAKE_CLAUDE_UP args: ` and its
|
|
8
|
+
* arguments, and ends by echoing its input back until that input closes.
|
|
9
|
+
* Unless the file
|
|
10
|
+
* `$HOME/fake-claude-hold-start` exists, it first reports `SessionStart`
|
|
11
|
+
* through the real reporter with session id `fake-1` and transcript
|
|
12
|
+
* `$HOME/fake-transcript.jsonl`, and when `$HOME/fake-claude-note` exists it
|
|
13
|
+
* files that file's text as a report labelled `decision`.
|
|
14
|
+
*/
|
|
15
|
+
export function buildStubMCPClaude(): string {
|
|
16
|
+
const cli = `"${process.execPath}" "${CLI_PATH}"`;
|
|
17
|
+
|
|
18
|
+
return `#!/usr/bin/env bash
|
|
19
|
+
echo $$ >> "$HOME/stub-pids"
|
|
20
|
+
echo "FAKE_CLAUDE_UP args: $@"
|
|
21
|
+
if [ -f "$HOME/fake-claude-hold-start" ]; then exec cat; fi
|
|
22
|
+
printf '{"hook_event_name":"SessionStart","session_id":"fake-1","transcript_path":"'"$HOME"'/fake-transcript.jsonl"}' | ${cli} hook-report
|
|
23
|
+
if [ -f "$HOME/fake-claude-note" ]; then ${cli} report note --label decision < "$HOME/fake-claude-note"; fi
|
|
24
|
+
exec cat
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
const CLI_PATH = join(import.meta.dir, '..', 'cli.ts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The script of a stand-in `grok` for the stdio MCP suites. It appends its
|
|
7
|
+
* pid to `$HOME/stub-pids`, prints `FAKE_GROK_UP args: ` and its arguments,
|
|
8
|
+
* reports `session_start` through the real reporter with session id
|
|
9
|
+
* `fake-grok-1` and its working directory, then echoes its input back until
|
|
10
|
+
* that input closes.
|
|
11
|
+
*/
|
|
12
|
+
export function buildStubMCPGrok(): string {
|
|
13
|
+
return `#!/usr/bin/env bash
|
|
14
|
+
echo $$ >> "$HOME/stub-pids"
|
|
15
|
+
echo "FAKE_GROK_UP args: $@"
|
|
16
|
+
printf '{"hookEventName":"session_start","sessionId":"fake-grok-1","cwd":"%s"}' "$PWD" | "${process.execPath}" "${CLI_PATH}" hook-report
|
|
17
|
+
exec cat
|
|
18
|
+
`;
|
|
19
|
+
}
|