@zgeoff/atc 2.11.0 → 2.12.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 +1 -1
- package/src/agents/agent-adapter.ts +51 -0
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/claude-adapter.ts +49 -2
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +43 -2
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +45 -2
- package/src/agents/grok-adapter.ts +19 -1
- package/src/cli.ts +18 -0
- package/src/client/daemon-client.ts +14 -2
- package/src/client/spawn-picker.ts +27 -4
- package/src/daemon/build-agent-list.ts +37 -1
- package/src/daemon/build-config-revision.ts +27 -0
- package/src/daemon/build-execution-targets.ts +34 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-scoped-context.ts +232 -0
- package/src/daemon/build-target-access.ts +33 -0
- package/src/daemon/build-target-forbidden-error.ts +13 -0
- package/src/daemon/build-target-identity.ts +22 -0
- package/src/daemon/build-target-list.ts +50 -0
- package/src/daemon/daemon-connection.ts +331 -95
- package/src/daemon/daemon.ts +452 -75
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +103 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/local-pty-provider.ts +83 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/restore-fleet.ts +66 -19
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +282 -65
- package/src/daemon/start-headless-run.ts +11 -0
- package/src/daemon/start-headless-turn.ts +11 -2
- package/src/daemon/target-access.ts +36 -0
- package/src/mcp/answer-mcp-request.ts +12 -4
- package/src/mcp/answer-rpc-request.ts +28 -1
- package/src/mcp/build-principal-caller.ts +15 -0
- package/src/mcp/build-spawn-descriptions.ts +45 -0
- package/src/mcp/build-tool-list.ts +32 -4
- package/src/mcp/mcp-tools.ts +98 -8
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +39 -7
- package/src/mcp/require-daemon-features.ts +7 -0
- package/src/mcp/run-tool.ts +63 -11
- package/src/mcp/types.ts +2 -0
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +24 -0
- package/src/protocol/protocol.ts +44 -11
- package/src/protocol/request-param-schemas.ts +60 -10
- package/src/shared/agent-session-id.ts +1 -1
- package/src/shared/collect-principals.ts +51 -0
- package/src/shared/collect-targets.ts +144 -0
- package/src/shared/config.ts +139 -14
- package/src/shared/daemon-id.ts +8 -0
- package/src/shared/format-json-kind.ts +21 -0
- package/src/shared/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/store/fleet-entry.ts +42 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/run-migrations.ts +254 -5
- package/src/store/state-store.ts +433 -31
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +239 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +103 -0
- package/src/workspace/sanitize-workspace-clone.ts +146 -0
- package/src/workspace/workspace-provenance.ts +11 -0
- package/src/workspace/workspace-source.ts +8 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { normalizeGitURL } from './normalize-git-url';
|
|
4
|
+
import { runGit } from './run-git';
|
|
5
|
+
import type { WorkspaceProvenance } from './workspace-provenance';
|
|
6
|
+
|
|
7
|
+
interface SanitizedClone {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly provenance: WorkspaceProvenance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SanitizeRefusal {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly code: 'invalid_git_url' | 'sanitize_failed';
|
|
15
|
+
readonly message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Strips a fresh clone of everything that could authenticate as, or run code
|
|
20
|
+
* for, the host it was cloned on, before the clone leaves that host. Objects,
|
|
21
|
+
* refs, HEAD, and the checked-out branch stay. The origin URL is reset to its
|
|
22
|
+
* token-free form; every credential setting, every http extra header, every
|
|
23
|
+
* remote URL carrying a credential, and every URL rewrite whose either side holds
|
|
24
|
+
* userinfo are removed from the repository config; hooks, reflogs (which
|
|
25
|
+
* record the clone URL), and any `.git-credentials` or `.netrc` file are
|
|
26
|
+
* deleted. The result is then checked: the history still reads and the
|
|
27
|
+
* config holds no credential.
|
|
28
|
+
*/
|
|
29
|
+
export async function sanitizeWorkspaceClone(
|
|
30
|
+
dir: string,
|
|
31
|
+
url: string,
|
|
32
|
+
): Promise<SanitizeRefusal | SanitizedClone> {
|
|
33
|
+
const normalized = normalizeGitURL(url);
|
|
34
|
+
|
|
35
|
+
if (!normalized.ok) {
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const gitDir = join(dir, '.git');
|
|
40
|
+
const configFile = join(gitDir, 'config');
|
|
41
|
+
|
|
42
|
+
for (const key of await collectCredentialKeys(configFile)) {
|
|
43
|
+
await runGit(['config', '--file', configFile, '--unset-all', key]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const setURL = await runGit([
|
|
47
|
+
'config',
|
|
48
|
+
'--file',
|
|
49
|
+
configFile,
|
|
50
|
+
'remote.origin.url',
|
|
51
|
+
normalized.url,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
if (setURL.exitCode !== 0) {
|
|
55
|
+
return { ok: false, code: 'sanitize_failed', message: setURL.stderr.trim() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await rm(join(gitDir, 'hooks'), { recursive: true, force: true });
|
|
59
|
+
await mkdir(join(gitDir, 'hooks'));
|
|
60
|
+
await rm(join(gitDir, 'logs'), { recursive: true, force: true });
|
|
61
|
+
|
|
62
|
+
for (const file of ['.git-credentials', '.netrc']) {
|
|
63
|
+
await rm(join(dir, file), { force: true });
|
|
64
|
+
await rm(join(gitDir, file), { force: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return verifyClone(dir, configFile, normalized.url);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every config key whose setting can carry a secret: credential helpers and
|
|
72
|
+
* usernames, http extra headers (where an Authorization header rides),
|
|
73
|
+
* remote URLs that lose something when stripped of credentials, and URL
|
|
74
|
+
* rewrites whose either side holds userinfo.
|
|
75
|
+
*/
|
|
76
|
+
async function collectCredentialKeys(configFile: string): Promise<string[]> {
|
|
77
|
+
const listed = await runGit(['config', '--file', configFile, '--list', '-z']);
|
|
78
|
+
|
|
79
|
+
const keys = new Set<string>();
|
|
80
|
+
|
|
81
|
+
for (const entry of listed.stdout.split('\0')) {
|
|
82
|
+
const newline = entry.indexOf('\n');
|
|
83
|
+
const key = newline === -1 ? entry : entry.slice(0, newline);
|
|
84
|
+
const value = newline === -1 ? '' : entry.slice(newline + 1);
|
|
85
|
+
|
|
86
|
+
if (isCredentialEntry(key.toLowerCase(), key, value)) {
|
|
87
|
+
keys.add(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...keys];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const URL_WITH_USERINFO = /^[a-z][a-z\d+.-]*:\/\/[^/]*@/iu;
|
|
95
|
+
|
|
96
|
+
function isCredentialEntry(lowered: string, key: string, value: string): boolean {
|
|
97
|
+
if (lowered.startsWith('credential.')) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (lowered.startsWith('http.') && lowered.endsWith('.extraheader')) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (lowered.startsWith('remote.') && /\.(?:url|pushurl)$/u.test(lowered)) {
|
|
106
|
+
const normalized = normalizeGitURL(value);
|
|
107
|
+
|
|
108
|
+
return !normalized.ok || normalized.url !== value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rewrite = /^url\.(?<base>.+)\.(?:insteadof|pushinsteadof)$/iu.exec(key);
|
|
112
|
+
|
|
113
|
+
if (rewrite !== null) {
|
|
114
|
+
return URL_WITH_USERINFO.test(rewrite.groups?.['base'] ?? '') || URL_WITH_USERINFO.test(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function verifyClone(
|
|
121
|
+
dir: string,
|
|
122
|
+
configFile: string,
|
|
123
|
+
url: string,
|
|
124
|
+
): Promise<SanitizeRefusal | SanitizedClone> {
|
|
125
|
+
const log = await runGit(['log', '-1', '--format=%H'], { cwd: dir });
|
|
126
|
+
|
|
127
|
+
if (log.exitCode !== 0) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
code: 'sanitize_failed',
|
|
131
|
+
message: `history unreadable: ${log.stderr.trim()}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const remaining = await collectCredentialKeys(configFile);
|
|
136
|
+
|
|
137
|
+
if (remaining.length > 0) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: 'sanitize_failed',
|
|
141
|
+
message: `config still holds ${remaining.length} credential entries`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { ok: true, provenance: { repoURL: url, sha: log.stdout.trim() } };
|
|
146
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a materialized workspace was built from: the token-free repository
|
|
3
|
+
* URL and the exact commit checked out, plus the image and harness version
|
|
4
|
+
* when the execution target reports them.
|
|
5
|
+
*/
|
|
6
|
+
export interface WorkspaceProvenance {
|
|
7
|
+
readonly repoURL: string;
|
|
8
|
+
readonly sha: string;
|
|
9
|
+
readonly image?: string;
|
|
10
|
+
readonly harnessVersion?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a session's workspace comes from: a directory on a host, which is
|
|
3
|
+
* resolved to a pushed commit before anything leaves that host, or a git
|
|
4
|
+
* repository and ref that the control side clones itself.
|
|
5
|
+
*/
|
|
6
|
+
export type WorkspaceSource =
|
|
7
|
+
| { readonly kind: 'path'; readonly host: string; readonly path: string }
|
|
8
|
+
| { readonly kind: 'git'; readonly url: string; readonly ref: string };
|