@fitlab-ai/agent-infra 0.9.3 → 0.9.5
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/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +170 -27
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +110 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +127 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -2,9 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
6
6
|
import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import * as p from '@clack/prompts';
|
|
9
10
|
import pc from 'picocolors';
|
|
10
11
|
import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
sandboxImageRefreshLabel,
|
|
21
22
|
sandboxLabel,
|
|
22
23
|
sandboxRuntimeCapabilityLabel,
|
|
24
|
+
sandboxTaskIdLabel,
|
|
25
|
+
sandboxWorkspaceModeLabel,
|
|
23
26
|
shareBranchDir,
|
|
24
27
|
shareCommonDir,
|
|
25
28
|
shellConfigDir,
|
|
@@ -44,7 +47,11 @@ import {
|
|
|
44
47
|
runSafeEngine,
|
|
45
48
|
runVerboseEngine
|
|
46
49
|
} from '../shell.ts';
|
|
47
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
parseSandboxWorkspaceIdentity,
|
|
52
|
+
resolveSandboxTarget,
|
|
53
|
+
sameSandboxWorkspaceIdentity
|
|
54
|
+
} from '../workspace-identity.ts';
|
|
48
55
|
import {
|
|
49
56
|
createSandboxCapabilityPlan,
|
|
50
57
|
runSandboxHooks,
|
|
@@ -64,16 +71,17 @@ import {
|
|
|
64
71
|
} from '../recovery.ts';
|
|
65
72
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
66
73
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
74
|
+
import {
|
|
75
|
+
assertSandboxTaskSource,
|
|
76
|
+
materializeSandboxControl,
|
|
77
|
+
materializeSandboxWorkspaceView
|
|
78
|
+
} from '../workspace-view.ts';
|
|
67
79
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
68
80
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
69
81
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
70
82
|
import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
|
|
71
83
|
import { removeDirRecursive } from '../../remove-dir.ts';
|
|
72
|
-
import {
|
|
73
|
-
prepareClaudeCredentials,
|
|
74
|
-
redactCommandError,
|
|
75
|
-
validateClaudeCredentialsEnvOverride
|
|
76
|
-
} from '../credentials.ts';
|
|
84
|
+
import { redactCommandError } from '../redaction.ts';
|
|
77
85
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
78
86
|
import {
|
|
79
87
|
buildImageSignature,
|
|
@@ -84,10 +92,6 @@ import {
|
|
|
84
92
|
parseRefreshTimestamp
|
|
85
93
|
} from '../image-build.ts';
|
|
86
94
|
|
|
87
|
-
export {
|
|
88
|
-
ensureCodexModelInheritance,
|
|
89
|
-
ensureCodexWorkspaceTrust
|
|
90
|
-
} from '../../agent-clients/adapters/codex-sandbox.ts';
|
|
91
95
|
|
|
92
96
|
const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
|
|
93
97
|
const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
|
|
@@ -133,7 +137,6 @@ type TmpfsSeedPlanEntry = TmpfsSeedEntry & {
|
|
|
133
137
|
volumeArgs: string[];
|
|
134
138
|
};
|
|
135
139
|
type RuntimeCheck = { name: string; cmd: string[] };
|
|
136
|
-
type JsonObject = Record<string, unknown>;
|
|
137
140
|
type GpgCache = { pub: Buffer; sec: Buffer } | null;
|
|
138
141
|
type ExecSyncOptions = ExecFileSyncOptions & {
|
|
139
142
|
input?: Buffer | string;
|
|
@@ -154,6 +157,12 @@ type HostShellConfig = {
|
|
|
154
157
|
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
155
158
|
};
|
|
156
159
|
|
|
160
|
+
function internalCliPath(): string {
|
|
161
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
162
|
+
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
163
|
+
return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
158
167
|
return tools.map((tool) => {
|
|
159
168
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -807,209 +816,6 @@ export function assertBranchAvailable(
|
|
|
807
816
|
}
|
|
808
817
|
}
|
|
809
818
|
|
|
810
|
-
function readHostJsonSafe(filePath: string): JsonObject | null {
|
|
811
|
-
if (!filePath || !fs.existsSync(filePath)) {
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
|
|
817
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as JsonObject : null;
|
|
818
|
-
} catch {
|
|
819
|
-
return null;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS = [
|
|
824
|
-
'model',
|
|
825
|
-
'fallbackModel',
|
|
826
|
-
'availableModels',
|
|
827
|
-
'modelOverrides',
|
|
828
|
-
'enforceAvailableModels',
|
|
829
|
-
'advisorModel',
|
|
830
|
-
'apiKeyHelper',
|
|
831
|
-
'effortLevel'
|
|
832
|
-
];
|
|
833
|
-
|
|
834
|
-
function isJsonObjectRecord(value: unknown): value is JsonObject {
|
|
835
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function mergeMissingStringEnvFields(target: JsonObject, source: JsonObject): boolean {
|
|
839
|
-
if (!isJsonObjectRecord(source.env)) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
if (Object.hasOwn(target, 'env') && !isJsonObjectRecord(target.env)) {
|
|
843
|
-
return false;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
let targetEnv = target.env as JsonObject | undefined;
|
|
847
|
-
let changed = false;
|
|
848
|
-
for (const [key, value] of Object.entries(source.env)) {
|
|
849
|
-
if (typeof value !== 'string' || value === '') {
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (!targetEnv) {
|
|
853
|
-
targetEnv = {};
|
|
854
|
-
target.env = targetEnv;
|
|
855
|
-
}
|
|
856
|
-
if (!Object.hasOwn(targetEnv, key)) {
|
|
857
|
-
targetEnv[key] = value;
|
|
858
|
-
changed = true;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return changed;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function mergeMissingTopLevelSettings(target: JsonObject, source: JsonObject): boolean {
|
|
865
|
-
let changed = false;
|
|
866
|
-
for (const key of CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS) {
|
|
867
|
-
if (!Object.hasOwn(source, key) || Object.hasOwn(target, key)) {
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
|
-
const value = source[key];
|
|
871
|
-
if (value === null || value === undefined || value === '') {
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
target[key] = value;
|
|
875
|
-
changed = true;
|
|
876
|
-
}
|
|
877
|
-
return changed;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
export function ensureClaudeOnboarding(toolDir: string, hostHomeDir?: string): void {
|
|
881
|
-
const claudeJsonPath = path.join(toolDir, '.claude.json');
|
|
882
|
-
let data: JsonObject & {
|
|
883
|
-
hasCompletedOnboarding?: boolean;
|
|
884
|
-
projects?: Record<string, { hasTrustDialogAccepted?: boolean }>;
|
|
885
|
-
model?: string;
|
|
886
|
-
} = {};
|
|
887
|
-
if (fs.existsSync(claudeJsonPath)) {
|
|
888
|
-
try {
|
|
889
|
-
data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')) as typeof data;
|
|
890
|
-
} catch {
|
|
891
|
-
// malformed JSON, start fresh
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
let changed = false;
|
|
895
|
-
if (!data.hasCompletedOnboarding) {
|
|
896
|
-
data.hasCompletedOnboarding = true;
|
|
897
|
-
changed = true;
|
|
898
|
-
}
|
|
899
|
-
if (!data.projects) {
|
|
900
|
-
data.projects = {};
|
|
901
|
-
changed = true;
|
|
902
|
-
}
|
|
903
|
-
if (!data.projects['/workspace']) {
|
|
904
|
-
data.projects['/workspace'] = {};
|
|
905
|
-
changed = true;
|
|
906
|
-
}
|
|
907
|
-
if (!data.projects['/workspace'].hasTrustDialogAccepted) {
|
|
908
|
-
data.projects['/workspace'].hasTrustDialogAccepted = true;
|
|
909
|
-
changed = true;
|
|
910
|
-
}
|
|
911
|
-
if (hostHomeDir) {
|
|
912
|
-
const hostClaudeJson = readHostJsonSafe(path.join(hostHomeDir, '.claude.json'));
|
|
913
|
-
if (
|
|
914
|
-
hostClaudeJson
|
|
915
|
-
&& typeof hostClaudeJson.model === 'string'
|
|
916
|
-
&& hostClaudeJson.model !== ''
|
|
917
|
-
&& !Object.hasOwn(data, 'model')
|
|
918
|
-
) {
|
|
919
|
-
data.model = hostClaudeJson.model;
|
|
920
|
-
changed = true;
|
|
921
|
-
}
|
|
922
|
-
// Claude Code launch-pins a default effort per model generation (for
|
|
923
|
-
// example xhigh for Opus 4.7). The saved effortLevel is honored only after
|
|
924
|
-
// a top-level boolean `unpin*LaunchEffort: true` flag unlocks it, so mirror
|
|
925
|
-
// those flags here with the existing first-write semantics.
|
|
926
|
-
//
|
|
927
|
-
// Pattern matching avoids one patch per future model generation. If
|
|
928
|
-
// Anthropic changes the naming convention, this block will no-op and should
|
|
929
|
-
// be revisited.
|
|
930
|
-
if (hostClaudeJson) {
|
|
931
|
-
for (const key of Object.keys(hostClaudeJson)) {
|
|
932
|
-
if (
|
|
933
|
-
/^unpin.*LaunchEffort$/.test(key)
|
|
934
|
-
&& hostClaudeJson[key] === true
|
|
935
|
-
&& !Object.hasOwn(data, key)
|
|
936
|
-
) {
|
|
937
|
-
data[key] = true;
|
|
938
|
-
changed = true;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
if (changed) {
|
|
944
|
-
fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 4), 'utf8');
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): void {
|
|
949
|
-
const settingsPath = path.join(toolDir, 'settings.json');
|
|
950
|
-
let data: JsonObject & { skipDangerousModePermissionPrompt?: boolean } = {};
|
|
951
|
-
if (fs.existsSync(settingsPath)) {
|
|
952
|
-
try {
|
|
953
|
-
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as typeof data;
|
|
954
|
-
} catch {
|
|
955
|
-
// malformed JSON, start fresh
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
let changed = false;
|
|
959
|
-
if (data.skipDangerousModePermissionPrompt !== true) {
|
|
960
|
-
data.skipDangerousModePermissionPrompt = true;
|
|
961
|
-
changed = true;
|
|
962
|
-
}
|
|
963
|
-
if (hostHomeDir) {
|
|
964
|
-
const hostSettings = readHostJsonSafe(path.join(hostHomeDir, '.claude', 'settings.json'));
|
|
965
|
-
if (hostSettings) {
|
|
966
|
-
changed = mergeMissingStringEnvFields(data, hostSettings) || changed;
|
|
967
|
-
changed = mergeMissingTopLevelSettings(data, hostSettings) || changed;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
if (changed) {
|
|
971
|
-
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
export function ensureOpenCodeModelInheritance(toolDir: string, hostHomeDir?: string): void {
|
|
976
|
-
if (!hostHomeDir) {
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const hostConfigPath = path.join(hostHomeDir, '.config', 'opencode', 'opencode.json');
|
|
981
|
-
const hostJson = readHostJsonSafe(hostConfigPath);
|
|
982
|
-
if (!hostJson) {
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const sandboxConfigPath = path.join(toolDir, 'opencode.json');
|
|
987
|
-
let sandboxJson: JsonObject = {};
|
|
988
|
-
if (fs.existsSync(sandboxConfigPath)) {
|
|
989
|
-
const existing = readHostJsonSafe(sandboxConfigPath);
|
|
990
|
-
if (!existing) {
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
sandboxJson = existing;
|
|
994
|
-
}
|
|
995
|
-
let changed = false;
|
|
996
|
-
for (const key of ['model', 'small_model']) {
|
|
997
|
-
if (Object.hasOwn(sandboxJson, key)) {
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
const value = hostJson[key];
|
|
1001
|
-
if (typeof value !== 'string' || value === '') {
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
sandboxJson[key] = value;
|
|
1005
|
-
changed = true;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
if (changed) {
|
|
1009
|
-
fs.writeFileSync(sandboxConfigPath, JSON.stringify(sandboxJson, null, 2), 'utf8');
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
819
|
export function sandboxAliasesPath(home: string): string {
|
|
1014
820
|
return hostJoin(home, '.agent-infra', 'aliases', 'sandbox.sh');
|
|
1015
821
|
}
|
|
@@ -1195,11 +1001,10 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1195
1001
|
}
|
|
1196
1002
|
|
|
1197
1003
|
validateSelinuxDisableEnv();
|
|
1198
|
-
validateClaudeCredentialsEnvOverride();
|
|
1199
|
-
|
|
1200
1004
|
const config = loadConfig();
|
|
1201
1005
|
const [branchOrTaskId = '', base] = positionals;
|
|
1202
|
-
const
|
|
1006
|
+
const target = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
|
|
1007
|
+
const branch = target.branch;
|
|
1203
1008
|
assertValidBranchName(branch);
|
|
1204
1009
|
const effectiveConfig = {
|
|
1205
1010
|
...config,
|
|
@@ -1214,20 +1019,31 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1214
1019
|
const capabilityPlan = createSandboxCapabilityPlan(effectiveConfig);
|
|
1215
1020
|
const tools = [...capabilityPlan.tools];
|
|
1216
1021
|
const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1022
|
+
const prepareHookResults = await runSandboxHooks({
|
|
1023
|
+
hooks: capabilityPlan.hooksByPhase.prepare,
|
|
1024
|
+
phase: 'prepare',
|
|
1025
|
+
context: {
|
|
1026
|
+
config: effectiveConfig,
|
|
1027
|
+
plan: capabilityPlan,
|
|
1028
|
+
create: {
|
|
1029
|
+
hostHome: effectiveConfig.home,
|
|
1030
|
+
hostEnv: { ...process.env },
|
|
1031
|
+
project: effectiveConfig.project,
|
|
1032
|
+
resolvedTools
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
runCommand: runBoundedSandboxHookCommand
|
|
1036
|
+
});
|
|
1037
|
+
const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
|
|
1038
|
+
if (prepareFailure) {
|
|
1039
|
+
throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
|
|
1040
|
+
}
|
|
1225
1041
|
const effectiveResolvedTools = resolvedTools;
|
|
1226
1042
|
const container = containerName(effectiveConfig, branch);
|
|
1227
1043
|
const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
|
|
1228
1044
|
const shareCommon = shareCommonDir(effectiveConfig);
|
|
1229
1045
|
const shareBranch = shareBranchDir(effectiveConfig, branch);
|
|
1230
|
-
const preparedDockerfile = prepareDockerfile(effectiveConfig);
|
|
1046
|
+
const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
|
|
1231
1047
|
const baseBranch = base ?? runSafe('git', ['-C', effectiveConfig.repoRoot, 'branch', '--show-current']);
|
|
1232
1048
|
const expectedImageSignature = buildImageSignature(preparedDockerfile, tools);
|
|
1233
1049
|
const engine = detectEngine(effectiveConfig);
|
|
@@ -1237,26 +1053,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1237
1053
|
p.log.info(
|
|
1238
1054
|
`Project: ${pc.bold(effectiveConfig.project)} | Branch: ${pc.bold(branch)} | Base: ${pc.bold(baseBranch || 'HEAD')}`
|
|
1239
1055
|
);
|
|
1240
|
-
|
|
1241
|
-
p.log.warn(
|
|
1242
|
-
'Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
|
|
1243
|
-
+ ' Claude Code is still installed in the image but will not be authenticated.\n'
|
|
1244
|
-
+ ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".'
|
|
1245
|
-
);
|
|
1056
|
+
for (const result of prepareHookResults) {
|
|
1057
|
+
if (result.status === 'warning' && result.message) p.log.warn(result.message);
|
|
1246
1058
|
}
|
|
1247
1059
|
|
|
1248
1060
|
try {
|
|
1249
1061
|
p.log.step('Checking container engine...');
|
|
1250
|
-
const prepareHookResults = await runSandboxHooks({
|
|
1251
|
-
hooks: capabilityPlan.hooksByPhase.prepare,
|
|
1252
|
-
phase: 'prepare',
|
|
1253
|
-
context: { config: effectiveConfig, plan: capabilityPlan },
|
|
1254
|
-
runCommand: runBoundedSandboxHookCommand
|
|
1255
|
-
});
|
|
1256
|
-
const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
|
|
1257
|
-
if (prepareFailure) {
|
|
1258
|
-
throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
|
|
1259
|
-
}
|
|
1260
1062
|
await ensureDocker(effectiveConfig, (detail: string) => {
|
|
1261
1063
|
p.log.info(` ${detail}`);
|
|
1262
1064
|
});
|
|
@@ -1431,6 +1233,33 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1431
1233
|
.filter((name) => existing.includes(name));
|
|
1432
1234
|
|
|
1433
1235
|
if (matchedContainers.length > 0) {
|
|
1236
|
+
for (const name of matchedContainers) {
|
|
1237
|
+
const rawLabels = runSafeEngine(engine, 'docker', [
|
|
1238
|
+
'inspect', '--format', '{{json .Config.Labels}}', name
|
|
1239
|
+
]).trim();
|
|
1240
|
+
let labels: Record<string, string> = {};
|
|
1241
|
+
try {
|
|
1242
|
+
labels = JSON.parse(rawLabels) as Record<string, string>;
|
|
1243
|
+
} catch {
|
|
1244
|
+
labels = {};
|
|
1245
|
+
}
|
|
1246
|
+
const existingIdentity = parseSandboxWorkspaceIdentity(labels, {
|
|
1247
|
+
mode: sandboxWorkspaceModeLabel(effectiveConfig),
|
|
1248
|
+
taskId: sandboxTaskIdLabel(effectiveConfig)
|
|
1249
|
+
});
|
|
1250
|
+
if (!sameSandboxWorkspaceIdentity(existingIdentity, target.workspace)) {
|
|
1251
|
+
const existingDescription = existingIdentity.mode === 'task-bound'
|
|
1252
|
+
? `task-bound:${existingIdentity.taskId}`
|
|
1253
|
+
: existingIdentity.mode;
|
|
1254
|
+
const requestedDescription = target.workspace.mode === 'task-bound'
|
|
1255
|
+
? `task-bound:${target.workspace.taskId}`
|
|
1256
|
+
: target.workspace.mode;
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
|
|
1259
|
+
+ `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1434
1263
|
message('Removing old container instance...');
|
|
1435
1264
|
for (const name of matchedContainers) {
|
|
1436
1265
|
runSafeEngine(engine, 'docker', ['stop', name]);
|
|
@@ -1485,6 +1314,8 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1485
1314
|
plan: capabilityPlan,
|
|
1486
1315
|
create: {
|
|
1487
1316
|
hostHome: effectiveConfig.home,
|
|
1317
|
+
hostEnv: { ...process.env },
|
|
1318
|
+
project: effectiveConfig.project,
|
|
1488
1319
|
resolvedTools: effectiveResolvedTools
|
|
1489
1320
|
}
|
|
1490
1321
|
},
|
|
@@ -1499,26 +1330,27 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1499
1330
|
?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`
|
|
1500
1331
|
);
|
|
1501
1332
|
}
|
|
1502
|
-
const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
1503
|
-
if (claudeCodeEntry) {
|
|
1504
|
-
ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1505
|
-
ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1506
|
-
// prepareClaudeCredentials wrote OAuth credentials or confirmed
|
|
1507
|
-
// provider/API settings are enough for Claude Code. If no usable
|
|
1508
|
-
// auth was present, the claude-code entry was removed above.
|
|
1509
|
-
}
|
|
1510
|
-
const opencodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'opencode');
|
|
1511
|
-
if (opencodeEntry) {
|
|
1512
|
-
// The TUI reads <toolDir>/opencode.json via OPENCODE_CONFIG pinned in tools.js.
|
|
1513
|
-
ensureOpenCodeModelInheritance(opencodeEntry.dir, effectiveConfig.home);
|
|
1514
|
-
}
|
|
1515
1333
|
const toolVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) =>
|
|
1516
1334
|
tool.tmpfs ? [] : ['-v', volumeArg(engine, dir, tool.containerMount)]
|
|
1517
1335
|
);
|
|
1518
1336
|
const tmpfsArgs = effectiveResolvedTools.flatMap(({ tool }) =>
|
|
1519
1337
|
tool.tmpfs ? buildTmpfsRunArgs(tool.containerMount, tool.tmpfs) : []
|
|
1520
1338
|
);
|
|
1521
|
-
const
|
|
1339
|
+
const workspaceView = materializeSandboxWorkspaceView({
|
|
1340
|
+
base: effectiveConfig.workspaceViewBase,
|
|
1341
|
+
project: effectiveConfig.project,
|
|
1342
|
+
container,
|
|
1343
|
+
identity: target.workspace
|
|
1344
|
+
});
|
|
1345
|
+
const control = materializeSandboxControl({
|
|
1346
|
+
base: effectiveConfig.controlBase,
|
|
1347
|
+
repoRoot: effectiveConfig.repoRoot,
|
|
1348
|
+
worktreeRoot: worktree,
|
|
1349
|
+
project: effectiveConfig.project,
|
|
1350
|
+
container,
|
|
1351
|
+
branch,
|
|
1352
|
+
identity: target.workspace
|
|
1353
|
+
});
|
|
1522
1354
|
hostShellConfig = prepareHostShellConfig({
|
|
1523
1355
|
home: effectiveConfig.home,
|
|
1524
1356
|
project: effectiveConfig.project,
|
|
@@ -1527,7 +1359,18 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1527
1359
|
});
|
|
1528
1360
|
const coreVolumes = sandboxCoreBindMounts(effectiveConfig, branch, {
|
|
1529
1361
|
worktree,
|
|
1530
|
-
shellConfigHostDir: hostShellConfig.hostDir
|
|
1362
|
+
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1363
|
+
workspaceViewRoot: workspaceView.root,
|
|
1364
|
+
controlDir: control.channelDir,
|
|
1365
|
+
...(target.workspace.mode === 'task-bound'
|
|
1366
|
+
? {
|
|
1367
|
+
taskSource: assertSandboxTaskSource(
|
|
1368
|
+
effectiveConfig.repoRoot,
|
|
1369
|
+
target.workspace.taskId
|
|
1370
|
+
),
|
|
1371
|
+
taskId: target.workspace.taskId
|
|
1372
|
+
}
|
|
1373
|
+
: {})
|
|
1531
1374
|
}).flatMap(({ hostPaths, containerPath, readOnly }) => [
|
|
1532
1375
|
'-v',
|
|
1533
1376
|
volumeArg(engine, hostPaths[0]!, containerPath, readOnly ? ':ro' : '')
|
|
@@ -1564,7 +1407,6 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1564
1407
|
])
|
|
1565
1408
|
);
|
|
1566
1409
|
|
|
1567
|
-
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
1568
1410
|
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1569
1411
|
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1570
1412
|
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
@@ -1591,6 +1433,20 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1591
1433
|
'--label',
|
|
1592
1434
|
`${sandboxBranchLabel(effectiveConfig)}=${branch}`,
|
|
1593
1435
|
'--label',
|
|
1436
|
+
`${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
|
|
1437
|
+
...(target.workspace.mode === 'task-bound'
|
|
1438
|
+
? [
|
|
1439
|
+
'--label',
|
|
1440
|
+
`${sandboxTaskIdLabel(effectiveConfig)}=${target.workspace.taskId}`,
|
|
1441
|
+
'-e',
|
|
1442
|
+
`AGENT_INFRA_TASK_ID=${target.workspace.taskId}`
|
|
1443
|
+
]
|
|
1444
|
+
: []),
|
|
1445
|
+
'-e',
|
|
1446
|
+
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1447
|
+
'-e',
|
|
1448
|
+
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1449
|
+
'--label',
|
|
1594
1450
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1595
1451
|
...coreVolumes,
|
|
1596
1452
|
...buildClipboardVolumeArgs(engine, effectiveConfig.home),
|
|
@@ -1611,6 +1467,16 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1611
1467
|
'/workspace',
|
|
1612
1468
|
effectiveConfig.imageName
|
|
1613
1469
|
]);
|
|
1470
|
+
const broker = spawn(
|
|
1471
|
+
process.execPath,
|
|
1472
|
+
[internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath],
|
|
1473
|
+
{ cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' }
|
|
1474
|
+
);
|
|
1475
|
+
broker.once('error', () => {
|
|
1476
|
+
// Fresh readiness verification below remains the source of truth;
|
|
1477
|
+
// consume detached spawn errors so they cannot crash the command.
|
|
1478
|
+
});
|
|
1479
|
+
broker.unref();
|
|
1614
1480
|
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1615
1481
|
} finally {
|
|
1616
1482
|
envFile.cleanup();
|
|
@@ -1713,10 +1579,11 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1713
1579
|
}
|
|
1714
1580
|
|
|
1715
1581
|
p.log.step('Verifying setup...');
|
|
1716
|
-
assertFreshSandboxReady({
|
|
1582
|
+
await assertFreshSandboxReady({
|
|
1717
1583
|
config: effectiveConfig,
|
|
1718
1584
|
engine,
|
|
1719
1585
|
branch,
|
|
1586
|
+
workspace: target.workspace,
|
|
1720
1587
|
container,
|
|
1721
1588
|
copiedEntries: createdTmpfsSeedPlan
|
|
1722
1589
|
});
|
|
@@ -12,23 +12,16 @@ import {
|
|
|
12
12
|
runBoundedSandboxHookCommand,
|
|
13
13
|
runSandboxHooks
|
|
14
14
|
} from '../agent-client-reconciler.ts';
|
|
15
|
-
import {
|
|
16
|
-
formatCredentialWarnings,
|
|
17
|
-
formatRemaining,
|
|
18
|
-
hasClaudeProviderAuth,
|
|
19
|
-
reconcileClaudeCredentials,
|
|
20
|
-
redactCommandError,
|
|
21
|
-
validateClaudeCredentialsEnvOverride
|
|
22
|
-
} from '../credentials.ts';
|
|
23
15
|
import { runInteractiveEngine } from '../shell.ts';
|
|
24
16
|
import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
|
|
17
|
+
import { redactCommandError } from '../redaction.ts';
|
|
25
18
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.ts';
|
|
26
19
|
import { detectHostTimezone } from '../host-timezone.ts';
|
|
27
20
|
import {
|
|
28
21
|
fetchSandboxRows,
|
|
29
|
-
resolveBranchArg,
|
|
30
22
|
selectSandboxContainer,
|
|
31
23
|
} from './list-running.ts';
|
|
24
|
+
import { resolveSandboxTarget } from '../workspace-identity.ts';
|
|
32
25
|
|
|
33
26
|
const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
|
|
34
27
|
|
|
@@ -98,37 +91,6 @@ export function runSandboxInteractive(params: {
|
|
|
98
91
|
return runBridge({ engine, dockerArgs, container, home });
|
|
99
92
|
}
|
|
100
93
|
|
|
101
|
-
export function formatCredentialSyncStatus(
|
|
102
|
-
result: ReturnType<typeof reconcileClaudeCredentials>,
|
|
103
|
-
isTTY = process.stderr.isTTY,
|
|
104
|
-
providerAuthAvailable = false
|
|
105
|
-
): string | null {
|
|
106
|
-
if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (result.status === 'STALE_ACCESS') {
|
|
110
|
-
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
111
|
-
}
|
|
112
|
-
if (result.status === 'MISSING') {
|
|
113
|
-
return 'Warning: Claude Code credentials missing on host. Run "claude /login" to authenticate.\n';
|
|
114
|
-
}
|
|
115
|
-
if (result.status === 'KEYCHAIN_WRITE_FAILED') {
|
|
116
|
-
return `Warning: A sandbox refresh produced newer credentials but host Keychain write failed (${formatCredentialWarnings(result.warnings)}). Run "ai sandbox refresh" again or "claude /status" on the host to retry.\n`;
|
|
117
|
-
}
|
|
118
|
-
if (result.status === 'KEYCHAIN_LOCKED' || result.status === 'KEYCHAIN_ERROR') {
|
|
119
|
-
return 'Warning: Host keychain is unavailable; Claude credential sync skipped. Run "ai sandbox refresh" for details.\n';
|
|
120
|
-
}
|
|
121
|
-
if (result.status === 'OK' && result.authoritative !== 'host') {
|
|
122
|
-
const message = `Synced Claude Code credentials from sandbox refresh back to host (expires in ${formatRemaining(result.expiresAt)})`;
|
|
123
|
-
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
124
|
-
}
|
|
125
|
-
if (result.status === 'OK' && result.filesWritten.length > 0) {
|
|
126
|
-
const message = `Synced Claude Code credentials from host Keychain (expires in ${formatRemaining(result.expiresAt)})`;
|
|
127
|
-
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
94
|
export function parseEnterArgs(args: string[]): {
|
|
133
95
|
target: string;
|
|
134
96
|
command: string[];
|
|
@@ -157,10 +119,10 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
157
119
|
}
|
|
158
120
|
|
|
159
121
|
const config = loadConfig();
|
|
160
|
-
validateClaudeCredentialsEnvOverride();
|
|
161
122
|
const engine = detectEngine(config);
|
|
162
123
|
const parsed = parseEnterArgs(args);
|
|
163
|
-
const
|
|
124
|
+
const target = resolveSandboxTarget(parsed.target, config.repoRoot);
|
|
125
|
+
const branch = target.branch;
|
|
164
126
|
assertValidBranchName(branch);
|
|
165
127
|
|
|
166
128
|
const { running, nonRunning } = fetchSandboxRows(
|
|
@@ -182,11 +144,12 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
182
144
|
config,
|
|
183
145
|
engine,
|
|
184
146
|
branch,
|
|
147
|
+
workspace: target.workspace,
|
|
185
148
|
row: found,
|
|
186
149
|
allowRecreate: parsed.recreate,
|
|
187
|
-
recreate: async (
|
|
150
|
+
recreate: async () => {
|
|
188
151
|
const { create } = await import('./create.ts');
|
|
189
|
-
await create([
|
|
152
|
+
await create([target.requestedRef, '--no-refresh']);
|
|
190
153
|
}
|
|
191
154
|
});
|
|
192
155
|
const container = ready.container;
|
|
@@ -195,7 +158,11 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
195
158
|
const enterHookResults = await runSandboxHooks({
|
|
196
159
|
hooks: capabilityPlan.hooksByPhase['before-enter'],
|
|
197
160
|
phase: 'before-enter',
|
|
198
|
-
context: {
|
|
161
|
+
context: {
|
|
162
|
+
config,
|
|
163
|
+
plan: capabilityPlan,
|
|
164
|
+
enter: { hostHome: config.home, hostEnv: { ...process.env } }
|
|
165
|
+
},
|
|
199
166
|
runCommand: runBoundedSandboxHookCommand
|
|
200
167
|
});
|
|
201
168
|
for (const result of enterHookResults) {
|
|
@@ -207,20 +174,7 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
207
174
|
`Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`
|
|
208
175
|
);
|
|
209
176
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (capabilityPlan.selectedAgentClients.some((adapter) => adapter.id === 'claude-code')) {
|
|
213
|
-
try {
|
|
214
|
-
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
215
|
-
const providerAuthAvailable = hasClaudeProviderAuth(config.home);
|
|
216
|
-
const result = reconcileClaudeCredentials(config.home);
|
|
217
|
-
const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
|
|
218
|
-
if (message) {
|
|
219
|
-
process.stderr.write(message);
|
|
220
|
-
}
|
|
221
|
-
} catch (error) {
|
|
222
|
-
process.stderr.write(`Warning: Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}\n`);
|
|
223
|
-
}
|
|
177
|
+
if (result.message && result.status !== 'warning') process.stderr.write(result.message);
|
|
224
178
|
}
|
|
225
179
|
|
|
226
180
|
const envFlags = [...terminalEnvFlags(), ...hostTimezoneEnvFlags()];
|