@zgeoff/atc 2.14.0 → 2.15.1
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 +2 -1
- package/src/agents/agent-adapter.ts +3 -38
- package/src/agents/build-hook-settings.ts +1 -1
- package/src/agents/claude-adapter.ts +3 -3
- package/src/agents/codex-adapter.ts +3 -3
- package/src/agents/gateway-adapter.ts +3 -3
- package/src/agents/grok-adapter.ts +3 -3
- package/src/agents/start-claude-headless-run.ts +1 -1
- package/src/client/boot-daemon.ts +2 -2
- package/src/client/collect-agent-picks.ts +1 -1
- package/src/client/daemon-client.ts +7 -23
- package/src/client/format-overlay-agent-mark.ts +1 -1
- package/src/client/index.ts +4 -2
- package/src/client/pick-tab-target.ts +2 -2
- package/src/client/spawn-picker.ts +1 -1
- package/src/client/to-mirror-session.ts +3 -3
- package/src/client/ui.ts +3 -3
- package/src/daemon/build-imp-provider.ts +70 -23
- package/src/daemon/build-message-trail-entry.ts +1 -1
- package/src/daemon/build-report-trail-entry.ts +1 -1
- package/src/daemon/build-scoped-context.ts +1 -1
- package/src/daemon/build-session-message-event.ts +1 -1
- package/src/daemon/daemon-connection.ts +9 -253
- package/src/daemon/daemon-context.ts +240 -0
- package/src/daemon/daemon.ts +9 -5
- package/src/daemon/execution-provider.ts +12 -0
- package/src/daemon/hooks.ts +6 -24
- package/src/daemon/idempotency-ledger.ts +78 -18
- package/src/daemon/imp-client-port.ts +83 -27
- package/src/daemon/imp-harness.ts +41 -0
- package/src/daemon/imp-provider.ts +5 -11
- package/src/daemon/local-pty-provider.ts +45 -0
- package/src/daemon/parse-hook-line.ts +1 -1
- package/src/daemon/read-imp-token.ts +30 -0
- package/src/daemon/sessions.ts +134 -107
- package/src/daemon/start-headless-turn.ts +3 -3
- package/src/daemon/start-session-bridge.ts +2 -2
- package/src/mcp/collect-clients.ts +1 -1
- package/src/mcp/collect-grants.ts +1 -1
- package/src/mcp/find-client-name.ts +1 -1
- package/src/mcp/mcp-auth-schema.ts +37 -0
- package/src/mcp/open-mcp-auth.ts +2 -2
- package/src/mcp/reconnecting-caller.ts +15 -6
- package/src/mcp/remove-client.ts +1 -1
- package/src/mcp/revoke-grant.ts +1 -1
- package/src/mcp/types.ts +0 -38
- package/src/mcp-http-server.ts +2 -1
- package/src/mcp-server.ts +3 -14
- package/src/protocol/adapter-event.ts +25 -0
- package/src/protocol/count-session-states.ts +16 -0
- package/src/protocol/daemon-channel.ts +17 -0
- package/src/protocol/hook-event.ts +11 -0
- package/src/protocol/line-decoder.ts +34 -0
- package/src/{shared → protocol}/open-bridge-socket.ts +4 -7
- package/src/protocol/pinned-group-key.ts +2 -0
- package/src/protocol/session-state.ts +5 -0
- package/src/protocol/sort-grouped-session-views.ts +29 -0
- package/src/protocol/sort-session-views.ts +41 -0
- package/src/protocol/sortable-session-view.ts +13 -0
- package/src/report.ts +1 -1
- package/src/run-bridge-tap.ts +2 -2
- package/src/shared/agent-id.ts +6 -0
- package/src/shared/collect-gateways.ts +1 -1
- package/src/shared/to-agent-id.ts +11 -0
- package/src/statusline.ts +1 -1
- package/src/store/fleet-entry.ts +6 -3
- package/src/store/state-store.ts +80 -19
- /package/src/{shared → protocol}/send-bridge-request.ts +0 -0
- /package/src/{store → shared}/bun-sqlite-driver.ts +0 -0
- /package/src/{agents → shared}/truncate-detail.ts +0 -0
- /package/src/{daemon → shared}/truncate-summary.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgeoff/atc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"description": "Terminal control tower for coding-agent sessions",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/atc#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/atc/issues",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"build:atc-bridge": "bun scripts/build-atc-bridge.ts && oxfmt src/agents/atc-bridge-files.ts && format-codemod --quiet src/agents/atc-bridge-files.ts",
|
|
25
25
|
"build:binaries": "bash scripts/build-binaries.sh",
|
|
26
26
|
"audit": "bun audit",
|
|
27
|
+
"check:imports": "bun scripts/check-imports.ts",
|
|
27
28
|
"deadcode": "knip",
|
|
28
29
|
"lint": "oxlint --type-aware --type-check --report-unused-disable-directives-severity error",
|
|
29
30
|
"lint:fix": "oxlint --type-aware --type-check --report-unused-disable-directives-severity error --fix",
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
2
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
3
|
+
import type { AgentID } from '../shared/agent-id';
|
|
2
4
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
3
5
|
import type { SessionID } from '../shared/session-id';
|
|
4
6
|
|
|
5
|
-
/**
|
|
6
|
-
* Which agent a session runs under: the key the adapter registry is looked
|
|
7
|
-
* up by. Every agent CLI supplies one, and so does every configured backend
|
|
8
|
-
* that drives a CLI it does not own, so two ids can share one kind.
|
|
9
|
-
*/
|
|
10
|
-
export type AgentID = string;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Missing and empty values become Claude so a fleet written before the agent
|
|
14
|
-
* column still restores as Claude. Any other string is returned as it stands,
|
|
15
|
-
* registered or not: an id whose adapter is gone must reach the caller intact
|
|
16
|
-
* so the session can be shown and refused, never quietly run as Claude.
|
|
17
|
-
*/
|
|
18
|
-
export function toAgentID(raw: unknown): AgentID {
|
|
19
|
-
return typeof raw === 'string' && raw !== '' ? raw : 'claude';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
7
|
export interface SpawnOptions {
|
|
23
8
|
readonly prompt: string;
|
|
24
9
|
|
|
@@ -64,26 +49,6 @@ export interface GuestSpawnPlan extends SpawnPlan {
|
|
|
64
49
|
readonly files: Readonly<Record<string, string>>;
|
|
65
50
|
}
|
|
66
51
|
|
|
67
|
-
export interface AdapterEvent {
|
|
68
|
-
kind: 'started' | 'needs-input' | 'turn-done' | 'prompt-submitted' | 'ended' | 'heartbeat';
|
|
69
|
-
agentSessionID?: AgentSessionID;
|
|
70
|
-
message?: string;
|
|
71
|
-
|
|
72
|
-
// Fuller activity text than message: what the agent last said or was
|
|
73
|
-
// asked, for briefing. Bounded by the adapter.
|
|
74
|
-
detail?: string;
|
|
75
|
-
|
|
76
|
-
// Opaque handle the adapter can later pull a session name from.
|
|
77
|
-
nameSource?: string;
|
|
78
|
-
|
|
79
|
-
// Claude resume-existence path. Distinct from nameSource: a naming
|
|
80
|
-
// handle is not a resume gate.
|
|
81
|
-
transcriptSource?: string;
|
|
82
|
-
|
|
83
|
-
// The agent's whole final message for a finished turn; detail holds a bounded preview of it.
|
|
84
|
-
result?: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
52
|
export interface TranscriptToolUse {
|
|
88
53
|
readonly name: string;
|
|
89
54
|
readonly input: string;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
4
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
4
5
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
5
6
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
6
7
|
import type { Config } from '../shared/config';
|
|
7
8
|
import { isRecord } from '../shared/report';
|
|
8
9
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
9
10
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
11
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
10
12
|
import type {
|
|
11
|
-
AdapterEvent,
|
|
12
13
|
AgentAdapter,
|
|
13
14
|
AgentProfile,
|
|
14
15
|
GuestPaths,
|
|
@@ -31,7 +32,6 @@ import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
|
31
32
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
32
33
|
import { planTypedLineInput } from './plan-typed-line-input';
|
|
33
34
|
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
34
|
-
import { truncateDetail } from './truncate-detail';
|
|
35
35
|
import { writeATCBridge } from './write-atc-bridge';
|
|
36
36
|
import { writeHookSettings } from './write-hook-settings';
|
|
37
37
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import type {
|
|
4
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
5
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
5
6
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
6
7
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
7
8
|
import type { Config } from '../shared/config';
|
|
8
9
|
import { isRecord } from '../shared/report';
|
|
9
10
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
10
11
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
12
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
11
13
|
import type {
|
|
12
|
-
AdapterEvent,
|
|
13
14
|
AgentAdapter,
|
|
14
15
|
AgentProfile,
|
|
15
16
|
NameUpdate,
|
|
@@ -22,7 +23,6 @@ import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
|
22
23
|
import { findFlagValue } from './find-flag-value';
|
|
23
24
|
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
24
25
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
25
|
-
import { truncateDetail } from './truncate-detail';
|
|
26
26
|
|
|
27
27
|
// Codex's hook payload keys, snake_case. An absent or wrong-typed field
|
|
28
28
|
// parses to undefined rather than failing the payload, so a broken reporter
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
2
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
3
|
+
import type { AgentID } from '../shared/agent-id';
|
|
2
4
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
3
5
|
import type { GatewayConfig } from '../shared/collect-gateways';
|
|
4
6
|
import type { Config } from '../shared/config';
|
|
5
7
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
6
8
|
import type {
|
|
7
|
-
AdapterEvent,
|
|
8
9
|
AgentAdapter,
|
|
9
|
-
AgentID,
|
|
10
10
|
AgentProfile,
|
|
11
11
|
HeadlessRunner,
|
|
12
12
|
NameUpdate,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import type {
|
|
4
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
5
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
5
6
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
6
7
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
7
8
|
import type { Config } from '../shared/config';
|
|
@@ -9,8 +10,8 @@ import { isRecord } from '../shared/report';
|
|
|
9
10
|
import type { SessionID } from '../shared/session-id';
|
|
10
11
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
11
12
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
13
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
12
14
|
import type {
|
|
13
|
-
AdapterEvent,
|
|
14
15
|
AgentAdapter,
|
|
15
16
|
AgentProfile,
|
|
16
17
|
NameUpdate,
|
|
@@ -22,7 +23,6 @@ import type {
|
|
|
22
23
|
import { normalizeHookEventName } from './normalize-hook-event';
|
|
23
24
|
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
24
25
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
25
|
-
import { truncateDetail } from './truncate-detail';
|
|
26
26
|
|
|
27
27
|
interface GrokSessionHookState {
|
|
28
28
|
latestPromptID?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { truncateSummary } from '../daemon/truncate-summary';
|
|
2
1
|
import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
3
2
|
import { isRecord } from '../shared/report';
|
|
3
|
+
import { truncateSummary } from '../shared/truncate-summary';
|
|
4
4
|
import { buildClaudeQueryOptions } from './build-claude-query-options';
|
|
5
5
|
import type { ClaudeHeadlessRunRequest } from './build-claude-query-options';
|
|
6
6
|
|
|
@@ -2,17 +2,17 @@ import { spawn as spawnChild } from 'node:child_process';
|
|
|
2
2
|
import type { ChildProcess } from 'node:child_process';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { toAgentID } from '../agents/agent-adapter';
|
|
6
|
-
import type { AgentID } from '../agents/agent-adapter';
|
|
7
5
|
import { DaemonError } from '../protocol/daemon-error';
|
|
8
6
|
import type { DaemonFeature } from '../protocol/daemon-features';
|
|
9
7
|
import { parseDaemonFeatures } from '../protocol/parse-daemon-features';
|
|
10
8
|
import { PROTOCOL_V } from '../protocol/protocol';
|
|
9
|
+
import type { AgentID } from '../shared/agent-id';
|
|
11
10
|
import { daemonPidFile, daemonRecordFile, daemonSocketPath } from '../shared/config';
|
|
12
11
|
import { findDaemonRecord } from '../shared/find-daemon-record';
|
|
13
12
|
import { getBuild } from '../shared/get-build';
|
|
14
13
|
import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
15
14
|
import { makeSingleFlight } from '../shared/make-single-flight';
|
|
15
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
16
16
|
import { DaemonClient } from './daemon-client';
|
|
17
17
|
import { formatProtocolMismatch } from './format-protocol-mismatch';
|
|
18
18
|
import type { ProtocolMismatch } from './format-protocol-mismatch';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { DaemonChannel } from '../protocol/daemon-channel';
|
|
1
2
|
import { DaemonError } from '../protocol/daemon-error';
|
|
3
|
+
import { LineDecoder } from '../protocol/line-decoder';
|
|
2
4
|
import { OutboundQueue } from '../protocol/outbound-queue';
|
|
3
5
|
import { PROTOCOL_V, decodeMessage, encodeMessage } from '../protocol/protocol';
|
|
4
6
|
import type { EventMsg, ResponseMsg } from '../protocol/protocol';
|
|
@@ -13,16 +15,14 @@ interface Pending {
|
|
|
13
15
|
* event callback. `open` connects the socket; `sendHello` completes the
|
|
14
16
|
* handshake and must come first.
|
|
15
17
|
*/
|
|
16
|
-
export class DaemonClient {
|
|
18
|
+
export class DaemonClient implements DaemonChannel {
|
|
17
19
|
onEvent: (event: EventMsg) => void = () => {};
|
|
18
20
|
|
|
19
21
|
onClose: () => void = () => {};
|
|
20
22
|
|
|
21
23
|
private queue: OutboundQueue | null = null;
|
|
22
24
|
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
private readonly decoder = new TextDecoder();
|
|
25
|
+
private readonly lines = new LineDecoder();
|
|
26
26
|
|
|
27
27
|
private nextID = 1;
|
|
28
28
|
|
|
@@ -41,7 +41,7 @@ export class DaemonClient {
|
|
|
41
41
|
unix: socketPath,
|
|
42
42
|
socket: {
|
|
43
43
|
data(_s, buf) {
|
|
44
|
-
client.applyChunk(
|
|
44
|
+
client.applyChunk(buf);
|
|
45
45
|
},
|
|
46
46
|
drain() {
|
|
47
47
|
client.queue?.drain();
|
|
@@ -99,25 +99,9 @@ export class DaemonClient {
|
|
|
99
99
|
this.drainPending('client closed');
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// Decodes with state kept across reads, so a multi-byte character split
|
|
103
|
-
// between two reads decodes whole.
|
|
104
102
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- a socket read buffer has no readonly form
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private applyChunk(chunk: string): void {
|
|
110
|
-
this.buffer += chunk;
|
|
111
|
-
|
|
112
|
-
const lines = this.buffer.split('\n');
|
|
113
|
-
|
|
114
|
-
this.buffer = lines.pop() ?? '';
|
|
115
|
-
|
|
116
|
-
for (const line of lines) {
|
|
117
|
-
if (line.trim() === '') {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
103
|
+
private applyChunk(buf: Uint8Array): void {
|
|
104
|
+
for (const line of this.lines.splitChunk(buf)) {
|
|
121
105
|
this.applyLine(line);
|
|
122
106
|
}
|
|
123
107
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { match } from 'ts-pattern';
|
|
2
2
|
import { createActor } from 'xstate';
|
|
3
|
-
import
|
|
4
|
-
import { countSessionStates, sortGroupedSessionViews, sortSessionViews } from '../daemon/sessions';
|
|
3
|
+
import { countSessionStates } from '../protocol/count-session-states';
|
|
5
4
|
import { DaemonError } from '../protocol/daemon-error';
|
|
6
5
|
import type { EventMsg } from '../protocol/protocol';
|
|
6
|
+
import { sortGroupedSessionViews } from '../protocol/sort-grouped-session-views';
|
|
7
|
+
import { sortSessionViews } from '../protocol/sort-session-views';
|
|
8
|
+
import type { AgentID } from '../shared/agent-id';
|
|
7
9
|
import { loadConfig } from '../shared/config';
|
|
8
10
|
import { makeSingleFlight } from '../shared/make-single-flight';
|
|
9
11
|
import { bootDaemonClient } from './boot-daemon';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SessionState } from '../
|
|
2
|
-
import { sortSessionViews } from '../
|
|
1
|
+
import type { SessionState } from '../protocol/session-state';
|
|
2
|
+
import { sortSessionViews } from '../protocol/sort-session-views';
|
|
3
3
|
|
|
4
4
|
interface TabCandidate {
|
|
5
5
|
readonly id: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import type { AgentID } from '../agents/agent-adapter';
|
|
3
2
|
import { DaemonError } from '../protocol/daemon-error';
|
|
3
|
+
import type { AgentID } from '../shared/agent-id';
|
|
4
4
|
import { loadConfig } from '../shared/config';
|
|
5
5
|
import { collectAgentPicks } from './collect-agent-picks';
|
|
6
6
|
import type { AgentPick } from './collect-agent-picks';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import type { AgentID } from '../
|
|
4
|
-
import
|
|
2
|
+
import type { SessionState } from '../protocol/session-state';
|
|
3
|
+
import type { AgentID } from '../shared/agent-id';
|
|
4
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
5
5
|
|
|
6
6
|
export interface MirrorSession {
|
|
7
7
|
id: string;
|
package/src/client/ui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import { PINNED_GROUP_KEY } from '../protocol/pinned-group-key';
|
|
2
|
+
import type { SessionState } from '../protocol/session-state';
|
|
3
|
+
import type { AgentID } from '../shared/agent-id';
|
|
4
4
|
import { RESET_INPUT_MODES } from '../shared/reset-input-modes';
|
|
5
5
|
import { formatDir } from './dirs';
|
|
6
6
|
import { formatOverlayAgentMark } from './format-overlay-agent-mark';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ImpClientPort } from './imp-client-port';
|
|
2
2
|
import { ImpProvider } from './imp-provider';
|
|
3
|
+
import { readImpToken } from './read-imp-token';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The provider of an `imp` target, or the problem that keeps it from one.
|
|
6
|
-
* A problem never includes
|
|
7
|
+
* A problem never includes a token or any other environment value.
|
|
7
8
|
*/
|
|
8
9
|
export interface ImpProviderBuild {
|
|
9
10
|
readonly provider: ImpProvider | null;
|
|
@@ -12,13 +13,16 @@ export interface ImpProviderBuild {
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The provider of target `id` from its `imp` options: `url` is where impd
|
|
15
|
-
* listens, and `tokenEnv`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* listens, and at most one of `tokenEnv` and `tokenFile` gives the token,
|
|
17
|
+
* so no credential sits in the config. `tokenEnv` holds the name of the
|
|
18
|
+
* daemon's environment variable that carries the token, read once here.
|
|
19
|
+
* `tokenFile` holds the path of a file that carries it, read here and again
|
|
20
|
+
* before each impd call and connection. `image`, `memoryMib`, `guestDir`,
|
|
21
|
+
* and `guestATC` pass through. No provider when `url` is missing, so the
|
|
22
|
+
* target lists and refuses every spawn. Both token options, either one not
|
|
23
|
+
* a non-empty string, an unset or empty variable, or an empty or unreadable
|
|
24
|
+
* file is a problem and leaves no provider; a target without either calls
|
|
25
|
+
* impd with no token.
|
|
22
26
|
*/
|
|
23
27
|
export function buildImpProvider(
|
|
24
28
|
id: string,
|
|
@@ -26,29 +30,18 @@ export function buildImpProvider(
|
|
|
26
30
|
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
27
31
|
): ImpProviderBuild {
|
|
28
32
|
const url = options['url'];
|
|
29
|
-
const tokenEnv = options['tokenEnv'];
|
|
30
33
|
|
|
31
34
|
if (typeof url !== 'string' || url === '') {
|
|
32
35
|
return { provider: null, problem: null };
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
provider: null,
|
|
38
|
-
problem: `target ${JSON.stringify(id)} must give tokenEnv as a non-empty string`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const token = tokenEnv === undefined ? null : env[tokenEnv];
|
|
38
|
+
const source = loadTokenSource(id, options, env);
|
|
43
39
|
|
|
44
|
-
if (
|
|
45
|
-
return {
|
|
46
|
-
provider: null,
|
|
47
|
-
problem: `target ${JSON.stringify(id)} reads its impd token from ${String(tokenEnv)}, which is unset or empty in the daemon's environment`,
|
|
48
|
-
};
|
|
40
|
+
if (typeof source === 'string') {
|
|
41
|
+
return { provider: null, problem: source };
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
const provider = new ImpProvider(new ImpClientPort({ url,
|
|
44
|
+
const provider = new ImpProvider(new ImpClientPort({ url, readToken: source.readToken }), {
|
|
52
45
|
...pickString(options, 'image'),
|
|
53
46
|
...pickString(options, 'guestDir'),
|
|
54
47
|
...pickString(options, 'guestATC'),
|
|
@@ -58,6 +51,60 @@ export function buildImpProvider(
|
|
|
58
51
|
return { provider, problem: null };
|
|
59
52
|
}
|
|
60
53
|
|
|
54
|
+
// How the port reads the token: a function, so a file token rereads.
|
|
55
|
+
interface TokenSource {
|
|
56
|
+
readonly readToken: () => string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The token source the options give, or the problem that keeps the target
|
|
60
|
+
// from one. A problem holds a variable's name or a file's path, never a
|
|
61
|
+
// token.
|
|
62
|
+
function loadTokenSource(
|
|
63
|
+
id: string,
|
|
64
|
+
options: Readonly<Record<string, unknown>>,
|
|
65
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
66
|
+
): TokenSource | string {
|
|
67
|
+
const tokenEnv = options['tokenEnv'];
|
|
68
|
+
const tokenFile = options['tokenFile'];
|
|
69
|
+
const target = JSON.stringify(id);
|
|
70
|
+
|
|
71
|
+
if (tokenEnv !== undefined && tokenFile !== undefined) {
|
|
72
|
+
return `target ${target} must give its impd token through tokenEnv or tokenFile, not both`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (tokenFile !== undefined) {
|
|
76
|
+
if (typeof tokenFile !== 'string' || tokenFile === '') {
|
|
77
|
+
return `target ${target} must give tokenFile as a non-empty string`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
readImpToken(tokenFile);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
84
|
+
|
|
85
|
+
return `target ${target} reads its impd token from a file it cannot use: ${detail}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { readToken: () => readImpToken(tokenFile) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (tokenEnv === undefined) {
|
|
92
|
+
return { readToken: () => null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof tokenEnv !== 'string' || tokenEnv === '') {
|
|
96
|
+
return `target ${target} must give tokenEnv as a non-empty string`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const token = env[tokenEnv];
|
|
100
|
+
|
|
101
|
+
if (token === undefined || token === '') {
|
|
102
|
+
return `target ${target} reads its impd token from ${tokenEnv}, which is unset or empty in the daemon's environment`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { readToken: () => token };
|
|
106
|
+
}
|
|
107
|
+
|
|
61
108
|
function pickString(
|
|
62
109
|
options: Readonly<Record<string, unknown>>,
|
|
63
110
|
key: 'image' | 'guestDir' | 'guestATC',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { truncateDetail } from '../agents/truncate-detail';
|
|
2
1
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
3
2
|
import type { SessionID } from '../shared/session-id';
|
|
3
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
4
4
|
import type { MessageRecord, MessageStatus } from '../store/message-record';
|
|
5
5
|
import type { TrailEntry } from '../store/trail-entry';
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { truncateDetail } from '../agents/truncate-detail';
|
|
2
1
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
3
2
|
import type { SessionID } from '../shared/session-id';
|
|
3
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
4
4
|
import type { TrailEntry } from '../store/trail-entry';
|
|
5
5
|
import type { NoteReport } from './parse-report';
|
|
6
6
|
|
|
@@ -3,7 +3,7 @@ import type { SessionID } from '../shared/session-id';
|
|
|
3
3
|
import { toSessionID } from '../shared/to-session-id';
|
|
4
4
|
import { buildTargetForbiddenError } from './build-target-forbidden-error';
|
|
5
5
|
import { buildTargetIdentity } from './build-target-identity';
|
|
6
|
-
import type { DaemonContext } from './daemon-
|
|
6
|
+
import type { DaemonContext } from './daemon-context';
|
|
7
7
|
import type { KeyedRequest } from './idempotency-ledger';
|
|
8
8
|
import type { TargetAccess, TargetGrant } from './target-access';
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { truncateDetail } from '../agents/truncate-detail';
|
|
2
1
|
import { PROTOCOL_V } from '../protocol/protocol';
|
|
3
2
|
import type { EventMsg } from '../protocol/protocol';
|
|
4
3
|
import type { SessionID } from '../shared/session-id';
|
|
4
|
+
import { truncateDetail } from '../shared/truncate-detail';
|
|
5
5
|
import type { MessageRecord } from '../store/message-record';
|
|
6
6
|
|
|
7
7
|
/**
|