@zgeoff/atc 2.38.0 → 2.39.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
CHANGED
|
@@ -86,6 +86,22 @@ export interface GuestSpawnPlan extends SpawnPlan {
|
|
|
86
86
|
readonly env?: Readonly<Record<string, string>>;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The project settings files the agent applies on top of what atc passes
|
|
91
|
+
* it, and the refusal for a file whose content would override or route
|
|
92
|
+
* around a sign-in through impd's broker, or null when it holds nothing
|
|
93
|
+
* that would. `files` are relative to the directory a harness starts in;
|
|
94
|
+
* `rootFiles` are relative to that directory, each directory above it, and
|
|
95
|
+
* the main checkout of the git worktree it lies in, since the agent reads
|
|
96
|
+
* them at a repository's root. `findRefusal` takes the file's absolute path
|
|
97
|
+
* on the host.
|
|
98
|
+
*/
|
|
99
|
+
export interface ProjectSettingsCheck {
|
|
100
|
+
readonly files: readonly string[];
|
|
101
|
+
readonly rootFiles: readonly string[];
|
|
102
|
+
readonly findRefusal: (path: string, content: string) => DaemonError | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
export interface TranscriptToolUse {
|
|
90
106
|
readonly name: string;
|
|
91
107
|
readonly input: string;
|
|
@@ -269,6 +285,11 @@ export interface AgentAdapter {
|
|
|
269
285
|
// The command that exits 0 inside a remote host when the agent there can
|
|
270
286
|
// sign in without a person. Absent: atc runs no check.
|
|
271
287
|
readonly planAuthCheck?: () => readonly string[];
|
|
288
|
+
|
|
289
|
+
// The project settings a remote launch behind impd's broker reads in
|
|
290
|
+
// the directory the harness starts in, before it starts. Absent: atc
|
|
291
|
+
// reads none.
|
|
292
|
+
readonly planProjectSettingsCheck?: () => ProjectSettingsCheck;
|
|
272
293
|
readonly normalizeHook: (e: HookEvent) => AdapterEvent;
|
|
273
294
|
readonly loadName: (
|
|
274
295
|
source: string,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
2
|
+
import { isSubscriptionOverrideVariable } from '../shared/is-subscription-override-variable';
|
|
3
|
+
import { isRecord } from '../shared/report';
|
|
4
|
+
import type { ProjectSettingsCheck } from './agent-adapter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The check of a Claude CLI session's project settings before a launch
|
|
8
|
+
* behind impd's broker. The CLI applies a repository's
|
|
9
|
+
* `.claude/settings.json` from the directory it starts in, and its
|
|
10
|
+
* `.claude/settings.local.json` from there and from the repository's root
|
|
11
|
+
* or a worktree's main checkout, once the folder is trusted; a person can
|
|
12
|
+
* accept that trust inside the session. Each file refuses the launch when
|
|
13
|
+
* its `env` block sets a variable that would override the sign-in or route
|
|
14
|
+
* around the broker, or when it sets `apiKeyHelper`. A file that is not a
|
|
15
|
+
* JSON object refuses it too, since what the CLI would take from it is
|
|
16
|
+
* unknown. A refusal holds the setting's name, never its value.
|
|
17
|
+
*/
|
|
18
|
+
export function buildClaudeProjectSettingsCheck(agent: string): ProjectSettingsCheck {
|
|
19
|
+
return {
|
|
20
|
+
files: ['.claude/settings.json', LOCAL_SETTINGS_FILE],
|
|
21
|
+
rootFiles: [LOCAL_SETTINGS_FILE],
|
|
22
|
+
findRefusal: (file, content) => {
|
|
23
|
+
const setting = findOverrideSetting(content);
|
|
24
|
+
|
|
25
|
+
if (setting === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const message =
|
|
30
|
+
setting === UNPARSEABLE
|
|
31
|
+
? `agent '${agent}' signs in through impd's broker, but ${file} is not a JSON object, so atc cannot check it for settings that would override that sign-in`
|
|
32
|
+
: `agent '${agent}' signs in through impd's broker, but ${file} sets ${setting}, which would override or route around that sign-in`;
|
|
33
|
+
|
|
34
|
+
return new DaemonError('auth_target_unsupported', message, {
|
|
35
|
+
agent,
|
|
36
|
+
problem: 'project_settings_conflict',
|
|
37
|
+
file,
|
|
38
|
+
setting,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LOCAL_SETTINGS_FILE = '.claude/settings.local.json';
|
|
45
|
+
const UNPARSEABLE = '(unparseable)';
|
|
46
|
+
|
|
47
|
+
// The first setting a project settings file holds that would override the
|
|
48
|
+
// sign-in, as `env.<variable>` or `apiKeyHelper`, the unparseable marker
|
|
49
|
+
// for content that is not a JSON object, or null when it holds none.
|
|
50
|
+
function findOverrideSetting(content: string): string | null {
|
|
51
|
+
let parsed: unknown;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(content);
|
|
55
|
+
} catch {
|
|
56
|
+
return UNPARSEABLE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) {
|
|
60
|
+
return UNPARSEABLE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parsed['apiKeyHelper'] !== undefined) {
|
|
64
|
+
return 'apiKeyHelper';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const env = parsed['env'];
|
|
68
|
+
|
|
69
|
+
const variable = isRecord(env)
|
|
70
|
+
? Object.keys(env).find((key) => isSubscriptionOverrideVariable(key))
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
return variable === undefined ? null : `env.${variable}`;
|
|
74
|
+
}
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
GuestSpawnPlan,
|
|
24
24
|
HeadlessRunner,
|
|
25
25
|
NameUpdate,
|
|
26
|
+
ProjectSettingsCheck,
|
|
26
27
|
ResumeCheck,
|
|
27
28
|
SpawnOptionSpecs,
|
|
28
29
|
SpawnOptions,
|
|
@@ -34,6 +35,7 @@ import { buildClaudeConfigSeed } from './build-claude-config-seed';
|
|
|
34
35
|
import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
|
|
35
36
|
import { buildClaudeMCPConfig } from './build-claude-mcp-config';
|
|
36
37
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
38
|
+
import { buildClaudeProjectSettingsCheck } from './build-claude-project-settings-check';
|
|
37
39
|
import { buildHookSettings } from './build-hook-settings';
|
|
38
40
|
import type { HookSettingsProfile } from './build-hook-settings';
|
|
39
41
|
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
@@ -226,6 +228,12 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
226
228
|
return buildClaudeConfigSeed(root);
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
// The repository's own settings files outrank what the session's user
|
|
232
|
+
// settings hold, so a launch behind impd's broker reads them first.
|
|
233
|
+
planProjectSettingsCheck(): ProjectSettingsCheck {
|
|
234
|
+
return buildClaudeProjectSettingsCheck(this.id);
|
|
235
|
+
}
|
|
236
|
+
|
|
229
237
|
// A settings file of the session's own per binding revision carries the
|
|
230
238
|
// placeholder, and so does the CLI's environment. The configured
|
|
231
239
|
// arguments go without a permission mode, so the mode the session's own
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
GuestSpawnPlan,
|
|
18
18
|
HeadlessRunner,
|
|
19
19
|
NameUpdate,
|
|
20
|
+
ProjectSettingsCheck,
|
|
20
21
|
ResumeCheck,
|
|
21
22
|
SpawnOptionSpecs,
|
|
22
23
|
SpawnOptions,
|
|
@@ -26,6 +27,7 @@ import { buildATCBridgeFiles } from './build-atc-bridge-files';
|
|
|
26
27
|
import { buildClaudeConfigSeed } from './build-claude-config-seed';
|
|
27
28
|
import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
|
|
28
29
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
30
|
+
import { buildClaudeProjectSettingsCheck } from './build-claude-project-settings-check';
|
|
29
31
|
import { buildHookSettings } from './build-hook-settings';
|
|
30
32
|
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
31
33
|
import { ClaudeAdapter } from './claude-adapter';
|
|
@@ -335,6 +337,12 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
335
337
|
];
|
|
336
338
|
}
|
|
337
339
|
|
|
340
|
+
// The repository's own settings files reach the CLI as well, so a launch
|
|
341
|
+
// behind impd's broker reads them first.
|
|
342
|
+
planProjectSettingsCheck(): ProjectSettingsCheck {
|
|
343
|
+
return buildClaudeProjectSettingsCheck(this.id);
|
|
344
|
+
}
|
|
345
|
+
|
|
338
346
|
private buildBrokerRefusal(reason: string): DaemonError {
|
|
339
347
|
return new DaemonError(
|
|
340
348
|
'auth_target_unsupported',
|
package/src/daemon/sessions.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { truncateDetail } from '../shared/truncate-detail';
|
|
|
27
27
|
import { truncateToBytes } from '../shared/truncate-to-bytes';
|
|
28
28
|
import type { FleetEntry, FleetEntryUpdate, FleetStore } from '../store/fleet-entry';
|
|
29
29
|
import type { SessionWorkspace } from '../store/workspace-materialization';
|
|
30
|
+
import { REPOSITORY_ENV_VARS } from '../workspace/repository-env-vars';
|
|
30
31
|
import type { BrokerAuthHost } from './broker-auth-host';
|
|
31
32
|
import { buildAuthBinding } from './build-auth-binding';
|
|
32
33
|
import type { AuthBinding } from './build-auth-binding';
|
|
@@ -241,6 +242,31 @@ const RESOLVE_DIR_SCRIPT = `p=$1; s=; while [ ! -d "$p" ]; do s=/\${p##*/}$s; p=
|
|
|
241
242
|
const REMOVE_DIR_SCRIPT =
|
|
242
243
|
'cd -P -- "$1" || exit 3; [ "$(pwd -P; printf x)" = "$2" ] || exit 4; find . -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + || exit 5; cd / && rmdir -- "$1"';
|
|
243
244
|
|
|
245
|
+
// Enters a directory as the host resolves it and prints, each followed by
|
|
246
|
+
// a NUL, every path where something, even a dangling symlink, stands: each
|
|
247
|
+
// start file in that directory, then each root file in that directory,
|
|
248
|
+
// every directory above it, and the main checkout of the git worktree it
|
|
249
|
+
// lies in. A `--` argument ends the start files. A directory that does not
|
|
250
|
+
// exist exits with the absent status.
|
|
251
|
+
const FIND_PROJECT_SETTINGS_SCRIPT = `cd -P -- "$1" 2>/dev/null || exit 3; shift; d=$(pwd -P)
|
|
252
|
+
emit() { if [ -e "$1" ] || [ -L "$1" ]; then printf '%s\\0' "$1"; fi; }
|
|
253
|
+
while [ "$#" -gt 0 ] && [ "$1" != -- ]; do emit "\${d%/}/$1"; shift; done; [ "$#" -gt 0 ] && shift
|
|
254
|
+
m=$(git -c safe.directory='*' -c core.fsmonitor=false rev-parse --path-format=absolute --git-common-dir 2>/dev/null) && m=\${m%/.git} || m=
|
|
255
|
+
for n in "$@"; do p=$d; while :; do emit "\${p%/}/$n"; [ "$p" = / ] && break; p=\${p%/*}; [ -n "$p" ] || p=/; done; [ -z "$m" ] || emit "\${m%/}/$n"; done
|
|
256
|
+
exit 0`;
|
|
257
|
+
|
|
258
|
+
// Prints at most one byte more than the largest project settings file atc
|
|
259
|
+
// reads, following a symlink, and fails with the special-file status for
|
|
260
|
+
// anything but a regular file there, such as a FIFO or a device that would
|
|
261
|
+
// never end.
|
|
262
|
+
const READ_SETTINGS_FILE = '[ -f "$1" ] || exit 4; exec head -c 1048577 -- "$1"';
|
|
263
|
+
const ABSENT_DIR_EXIT = 3;
|
|
264
|
+
|
|
265
|
+
// Runs git on the directory it starts in, whatever repository the daemon's
|
|
266
|
+
// environment pins git to.
|
|
267
|
+
const GIT_ENV_ARGV = ['env', ...[...REPOSITORY_ENV_VARS].flatMap((name) => ['-u', name])];
|
|
268
|
+
const MAX_SETTINGS_BYTES = 1_048_576;
|
|
269
|
+
|
|
244
270
|
// A readied host's harness plan, and the auth attempt that provisioned the
|
|
245
271
|
// host, if one did.
|
|
246
272
|
interface HarnessSetup {
|
|
@@ -693,6 +719,7 @@ export class SessionManager {
|
|
|
693
719
|
...(s.effort === undefined ? {} : { effort: s.effort }),
|
|
694
720
|
},
|
|
695
721
|
authSetup,
|
|
722
|
+
s.cwd,
|
|
696
723
|
);
|
|
697
724
|
|
|
698
725
|
plan = setup.plan;
|
|
@@ -999,6 +1026,10 @@ export class SessionManager {
|
|
|
999
1026
|
}
|
|
1000
1027
|
}
|
|
1001
1028
|
|
|
1029
|
+
// A spawn that builds its workspace reads the workspace's project
|
|
1030
|
+
// settings once it is built, not when the host is readied before it.
|
|
1031
|
+
const startDir = materialize === null ? cwd : null;
|
|
1032
|
+
|
|
1002
1033
|
const setupHost = () =>
|
|
1003
1034
|
this.setupHarnessOnHost(
|
|
1004
1035
|
adapter,
|
|
@@ -1008,9 +1039,17 @@ export class SessionManager {
|
|
|
1008
1039
|
target,
|
|
1009
1040
|
{ prompt, resume, ...overrides },
|
|
1010
1041
|
authSetup,
|
|
1042
|
+
startDir,
|
|
1011
1043
|
hostKey === id,
|
|
1012
1044
|
);
|
|
1013
1045
|
|
|
1046
|
+
// A launch behind the broker reads the project settings of the clone it
|
|
1047
|
+
// starts in, once the clone is on the host and before it is trusted.
|
|
1048
|
+
const checkWorkspace =
|
|
1049
|
+
auth === null
|
|
1050
|
+
? null
|
|
1051
|
+
: (root: string) => this.requireProjectSettings(adapter, provider, hostKey, target, root);
|
|
1052
|
+
|
|
1014
1053
|
// Takes back trust a spawn accepted in the user's own agent config when
|
|
1015
1054
|
// the spawn fails before its harness starts.
|
|
1016
1055
|
const localTrust: { remove: (() => Promise<void>) | null } = { remove: null };
|
|
@@ -1061,6 +1100,7 @@ export class SessionManager {
|
|
|
1061
1100
|
execution.identity,
|
|
1062
1101
|
materialize,
|
|
1063
1102
|
setupHost,
|
|
1103
|
+
checkWorkspace,
|
|
1064
1104
|
trustWorkspace,
|
|
1065
1105
|
);
|
|
1066
1106
|
});
|
|
@@ -1591,7 +1631,9 @@ export class SessionManager {
|
|
|
1591
1631
|
// Materializes a spawn's workspace, readying its host once the source
|
|
1592
1632
|
// resolves, or after the workspace for a directory that runs as it
|
|
1593
1633
|
// stands, and returns the directory it created, null for one that runs
|
|
1594
|
-
// as it stands. A
|
|
1634
|
+
// as it stands. A clone is checked and then trusted, either as the caller
|
|
1635
|
+
// asks, and a failure there removes it. A failure once the host is ready
|
|
1636
|
+
// takes it back.
|
|
1595
1637
|
private async materializeOnSpawnHost(
|
|
1596
1638
|
provider: ExecutionProvider,
|
|
1597
1639
|
id: SessionID,
|
|
@@ -1601,6 +1643,7 @@ export class SessionManager {
|
|
|
1601
1643
|
targetIdentity: string,
|
|
1602
1644
|
materialize: SpawnMaterializer,
|
|
1603
1645
|
setupHost: () => Promise<HarnessSetup>,
|
|
1646
|
+
checkWorkspace: ((root: string) => Promise<void>) | null,
|
|
1604
1647
|
trustWorkspace: ((root: string) => Promise<void>) | null,
|
|
1605
1648
|
): Promise<{
|
|
1606
1649
|
readonly setup: HarnessSetup;
|
|
@@ -1644,16 +1687,17 @@ export class SessionManager {
|
|
|
1644
1687
|
|
|
1645
1688
|
readied.setup ??= await setupHost();
|
|
1646
1689
|
|
|
1647
|
-
if (trustWorkspace !== null) {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
1690
|
+
if (trustWorkspace !== null && (materialized === null || readied.root === null)) {
|
|
1691
|
+
throw new DaemonError(
|
|
1692
|
+
'bad_args',
|
|
1693
|
+
'trustClonedWorkspace requires a successfully cloned workspace',
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1654
1696
|
|
|
1697
|
+
if (materialized !== null && readied.root !== null) {
|
|
1655
1698
|
try {
|
|
1656
|
-
await
|
|
1699
|
+
await checkWorkspace?.(readied.root);
|
|
1700
|
+
await trustWorkspace?.(readied.root);
|
|
1657
1701
|
} catch (error) {
|
|
1658
1702
|
let removed = false;
|
|
1659
1703
|
|
|
@@ -1665,7 +1709,7 @@ export class SessionManager {
|
|
|
1665
1709
|
|
|
1666
1710
|
if (!removed) {
|
|
1667
1711
|
throw new EffectRemainsError(
|
|
1668
|
-
'workspace
|
|
1712
|
+
'workspace setup failed and its clone could not be removed',
|
|
1669
1713
|
{ cause: error },
|
|
1670
1714
|
);
|
|
1671
1715
|
}
|
|
@@ -1906,9 +1950,10 @@ export class SessionManager {
|
|
|
1906
1950
|
target: string,
|
|
1907
1951
|
options: SpawnOptions,
|
|
1908
1952
|
auth: HarnessAuthSetup | null,
|
|
1953
|
+
dir: string,
|
|
1909
1954
|
): Promise<{ readonly plan: HarnessPlan; readonly attemptID: string | null }> {
|
|
1910
1955
|
return this.withHostReadying(hostKey, () =>
|
|
1911
|
-
this.setupHarnessOnHost(adapter, provider, id, hostKey, target, options, auth, false),
|
|
1956
|
+
this.setupHarnessOnHost(adapter, provider, id, hostKey, target, options, auth, dir, false),
|
|
1912
1957
|
);
|
|
1913
1958
|
}
|
|
1914
1959
|
|
|
@@ -1938,6 +1983,11 @@ export class SessionManager {
|
|
|
1938
1983
|
options: SpawnOptions,
|
|
1939
1984
|
auth: HarnessAuthSetup | null,
|
|
1940
1985
|
|
|
1986
|
+
// The directory the harness starts in, whose project settings a launch
|
|
1987
|
+
// behind the broker reads once the host is ready; null for a spawn
|
|
1988
|
+
// whose workspace is built after this, which reads them once it is.
|
|
1989
|
+
cwd: string | null,
|
|
1990
|
+
|
|
1941
1991
|
// Whether the host is a spawn's new host of its own, which a failure
|
|
1942
1992
|
// once it is readied destroys.
|
|
1943
1993
|
isNewHost: boolean,
|
|
@@ -2000,6 +2050,10 @@ export class SessionManager {
|
|
|
2000
2050
|
|
|
2001
2051
|
try {
|
|
2002
2052
|
await this.setupGuest(adapter, provider, hostKey, target, dir, plan.files);
|
|
2053
|
+
|
|
2054
|
+
if (auth !== null && cwd !== null) {
|
|
2055
|
+
await this.requireProjectSettings(adapter, provider, hostKey, target, cwd);
|
|
2056
|
+
}
|
|
2003
2057
|
} catch (error) {
|
|
2004
2058
|
if (attemptID !== null || isNewHost) {
|
|
2005
2059
|
await this.destroyFailedSpawnHost(provider, id, hostKey, attemptID);
|
|
@@ -2135,6 +2189,84 @@ export class SessionManager {
|
|
|
2135
2189
|
}
|
|
2136
2190
|
}
|
|
2137
2191
|
|
|
2192
|
+
// Reads each project settings file the agent applies for a harness behind
|
|
2193
|
+
// the broker, from the directory it starts in as the host resolves it, and
|
|
2194
|
+
// throws the agent's refusal for one that would override its sign-in. An
|
|
2195
|
+
// absent file passes; one the host holds but cannot read, that is not a
|
|
2196
|
+
// regular file, or that is larger than any settings file refuses, since
|
|
2197
|
+
// what the agent would take from it is unknown. A relative directory is
|
|
2198
|
+
// under the host's home.
|
|
2199
|
+
private async requireProjectSettings(
|
|
2200
|
+
adapter: AgentAdapter,
|
|
2201
|
+
provider: ExecutionProvider,
|
|
2202
|
+
hostKey: SessionID,
|
|
2203
|
+
target: string,
|
|
2204
|
+
cwd: string,
|
|
2205
|
+
): Promise<void> {
|
|
2206
|
+
const check = adapter.planProjectSettingsCheck?.();
|
|
2207
|
+
|
|
2208
|
+
if (check === undefined) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// The host resolves the directory as given, since a lexical join would
|
|
2213
|
+
// drop a parent step after a symlink that the host follows.
|
|
2214
|
+
const home = posix.isAbsolute(cwd) ? null : await this.resolveHostHome(provider, hostKey);
|
|
2215
|
+
const dir = home === null ? cwd : `${home}/${cwd}`;
|
|
2216
|
+
|
|
2217
|
+
const found = await provider.runCommand({
|
|
2218
|
+
argv: [
|
|
2219
|
+
...GIT_ENV_ARGV,
|
|
2220
|
+
'sh',
|
|
2221
|
+
'-c',
|
|
2222
|
+
FIND_PROJECT_SETTINGS_SCRIPT,
|
|
2223
|
+
'sh',
|
|
2224
|
+
dir,
|
|
2225
|
+
...check.files,
|
|
2226
|
+
'--',
|
|
2227
|
+
...check.rootFiles,
|
|
2228
|
+
],
|
|
2229
|
+
cwd: '/',
|
|
2230
|
+
host: hostKey,
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
if (found.exitCode === ABSENT_DIR_EXIT) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
if (found.exitCode !== 0) {
|
|
2238
|
+
throw this.buildUnreadableSettingsRefusal(adapter.id, target, dir);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
const paths = new Set(found.stdout.split('\0').filter((path) => path !== ''));
|
|
2242
|
+
|
|
2243
|
+
for (const path of paths) {
|
|
2244
|
+
const read = await provider.runCommand({
|
|
2245
|
+
argv: ['sh', '-c', READ_SETTINGS_FILE, 'sh', path],
|
|
2246
|
+
cwd: '/',
|
|
2247
|
+
host: hostKey,
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
if (read.exitCode !== 0 || Buffer.byteLength(read.stdout) > MAX_SETTINGS_BYTES) {
|
|
2251
|
+
throw this.buildUnreadableSettingsRefusal(adapter.id, target, path);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
const refusal = check.findRefusal(path, read.stdout);
|
|
2255
|
+
|
|
2256
|
+
if (refusal !== null) {
|
|
2257
|
+
throw new DaemonError(refusal.code, refusal.message, { ...refusal.data, target });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
private buildUnreadableSettingsRefusal(agent: string, target: string, path: string): DaemonError {
|
|
2263
|
+
return new DaemonError(
|
|
2264
|
+
'auth_target_unsupported',
|
|
2265
|
+
`agent '${agent}' signs in through impd's broker on target '${target}', but atc cannot read ${path} there as a settings file to check it for settings that would override that sign-in`,
|
|
2266
|
+
{ agent, target, problem: 'project_settings_unreadable', file: path },
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2138
2270
|
// A sub-session runs on its parent's host when its resolved target, name
|
|
2139
2271
|
// and identity both, is the one its parent is bound to and that target's
|
|
2140
2272
|
// hosts have a lifecycle, so one host serves a top-level session and
|