@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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, inspectClaudeKeychainStatus, prepareClaudeCredentials, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from './claude-code-credentials.js';
|
|
4
|
+
function readHostJsonSafe(filePath) {
|
|
5
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
9
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
10
|
+
? parsed
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS = [
|
|
18
|
+
'model',
|
|
19
|
+
'fallbackModel',
|
|
20
|
+
'availableModels',
|
|
21
|
+
'modelOverrides',
|
|
22
|
+
'enforceAvailableModels',
|
|
23
|
+
'advisorModel',
|
|
24
|
+
'apiKeyHelper',
|
|
25
|
+
'effortLevel'
|
|
26
|
+
];
|
|
27
|
+
function isJsonObjectRecord(value) {
|
|
28
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function mergeMissingStringEnvFields(target, source) {
|
|
31
|
+
if (!isJsonObjectRecord(source.env))
|
|
32
|
+
return false;
|
|
33
|
+
if (Object.hasOwn(target, 'env') && !isJsonObjectRecord(target.env))
|
|
34
|
+
return false;
|
|
35
|
+
let targetEnv = target.env;
|
|
36
|
+
let changed = false;
|
|
37
|
+
for (const [key, value] of Object.entries(source.env)) {
|
|
38
|
+
if (typeof value !== 'string' || value === '')
|
|
39
|
+
continue;
|
|
40
|
+
if (!targetEnv) {
|
|
41
|
+
targetEnv = {};
|
|
42
|
+
target.env = targetEnv;
|
|
43
|
+
}
|
|
44
|
+
if (!Object.hasOwn(targetEnv, key)) {
|
|
45
|
+
targetEnv[key] = value;
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return changed;
|
|
50
|
+
}
|
|
51
|
+
function mergeMissingTopLevelSettings(target, source) {
|
|
52
|
+
let changed = false;
|
|
53
|
+
for (const key of CLAUDE_SETTINGS_INHERIT_TOP_LEVEL_KEYS) {
|
|
54
|
+
if (!Object.hasOwn(source, key) || Object.hasOwn(target, key))
|
|
55
|
+
continue;
|
|
56
|
+
const value = source[key];
|
|
57
|
+
if (value === null || value === undefined || value === '')
|
|
58
|
+
continue;
|
|
59
|
+
target[key] = value;
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
return changed;
|
|
63
|
+
}
|
|
64
|
+
function requireCreateContext(context, hookId) {
|
|
65
|
+
if (!context)
|
|
66
|
+
throw new Error(`${hookId} requires create context`);
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
function requireEnterContext(context, hookId) {
|
|
70
|
+
if (!context)
|
|
71
|
+
throw new Error(`${hookId} requires enter context`);
|
|
72
|
+
return context;
|
|
73
|
+
}
|
|
74
|
+
export function ensureClaudeOnboarding(toolDir, hostHomeDir) {
|
|
75
|
+
const claudeJsonPath = path.join(toolDir, '.claude.json');
|
|
76
|
+
let data = {};
|
|
77
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Malformed JSON is replaced with a valid managed sandbox baseline.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let changed = false;
|
|
86
|
+
if (!data.hasCompletedOnboarding) {
|
|
87
|
+
data.hasCompletedOnboarding = true;
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
if (!data.projects) {
|
|
91
|
+
data.projects = {};
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
if (!data.projects['/workspace']) {
|
|
95
|
+
data.projects['/workspace'] = {};
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
if (!data.projects['/workspace'].hasTrustDialogAccepted) {
|
|
99
|
+
data.projects['/workspace'].hasTrustDialogAccepted = true;
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
if (hostHomeDir) {
|
|
103
|
+
const hostClaudeJson = readHostJsonSafe(path.join(hostHomeDir, '.claude.json'));
|
|
104
|
+
if (hostClaudeJson
|
|
105
|
+
&& typeof hostClaudeJson.model === 'string'
|
|
106
|
+
&& hostClaudeJson.model !== ''
|
|
107
|
+
&& !Object.hasOwn(data, 'model')) {
|
|
108
|
+
data.model = hostClaudeJson.model;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
if (hostClaudeJson) {
|
|
112
|
+
for (const key of Object.keys(hostClaudeJson)) {
|
|
113
|
+
if (/^unpin.*LaunchEffort$/.test(key)
|
|
114
|
+
&& hostClaudeJson[key] === true
|
|
115
|
+
&& !Object.hasOwn(data, key)) {
|
|
116
|
+
data[key] = true;
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (changed)
|
|
123
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 4), 'utf8');
|
|
124
|
+
}
|
|
125
|
+
export function ensureClaudeSettings(toolDir, hostHomeDir) {
|
|
126
|
+
const settingsPath = path.join(toolDir, 'settings.json');
|
|
127
|
+
let data = {};
|
|
128
|
+
if (fs.existsSync(settingsPath)) {
|
|
129
|
+
try {
|
|
130
|
+
data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Malformed JSON is replaced with a valid managed sandbox baseline.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let changed = false;
|
|
137
|
+
if (data.skipDangerousModePermissionPrompt !== true) {
|
|
138
|
+
data.skipDangerousModePermissionPrompt = true;
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
if (hostHomeDir) {
|
|
142
|
+
const hostSettings = readHostJsonSafe(path.join(hostHomeDir, '.claude', 'settings.json'));
|
|
143
|
+
if (hostSettings) {
|
|
144
|
+
changed = mergeMissingStringEnvFields(data, hostSettings) || changed;
|
|
145
|
+
changed = mergeMissingTopLevelSettings(data, hostSettings) || changed;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (changed)
|
|
149
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
|
|
150
|
+
}
|
|
151
|
+
export function formatCredentialSyncStatus(result, isTTY = process.stderr.isTTY, providerAuthAvailable = false) {
|
|
152
|
+
if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING'))
|
|
153
|
+
return null;
|
|
154
|
+
if (result.status === 'STALE_ACCESS') {
|
|
155
|
+
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
156
|
+
}
|
|
157
|
+
if (result.status === 'MISSING') {
|
|
158
|
+
return 'Warning: Claude Code credentials missing on host. Run "claude /login" to authenticate.\n';
|
|
159
|
+
}
|
|
160
|
+
if (result.status === 'KEYCHAIN_WRITE_FAILED') {
|
|
161
|
+
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`;
|
|
162
|
+
}
|
|
163
|
+
if (result.status === 'KEYCHAIN_LOCKED' || result.status === 'KEYCHAIN_ERROR') {
|
|
164
|
+
return 'Warning: Host keychain is unavailable; Claude credential sync skipped. Run "ai sandbox refresh" for details.\n';
|
|
165
|
+
}
|
|
166
|
+
if (result.status === 'OK' && result.authoritative !== 'host') {
|
|
167
|
+
const message = `Synced Claude Code credentials from sandbox refresh back to host (expires in ${formatRemaining(result.expiresAt)})`;
|
|
168
|
+
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
169
|
+
}
|
|
170
|
+
if (result.status === 'OK' && result.filesWritten.length > 0) {
|
|
171
|
+
const message = `Synced Claude Code credentials from host Keychain (expires in ${formatRemaining(result.expiresAt)})`;
|
|
172
|
+
return isTTY ? `\x1b[2m${message}\x1b[0m\n` : `${message}\n`;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const claudeCodeCredentialPreflightHook = {
|
|
177
|
+
id: 'claude-code-credential-preflight',
|
|
178
|
+
phase: 'prepare',
|
|
179
|
+
run: async ({ create }) => {
|
|
180
|
+
const { hostHome, hostEnv, project, resolvedTools } = requireCreateContext(create, 'claude-code-credential-preflight');
|
|
181
|
+
try {
|
|
182
|
+
validateClaudeCredentialsEnvOverride(hostEnv);
|
|
183
|
+
const outcome = prepareClaudeCredentials(hostHome, project, [...resolvedTools], undefined, undefined, (home) => inspectClaudeKeychainStatus(home, undefined, { envFn: () => ({ ...hostEnv }) }));
|
|
184
|
+
if (outcome.status === 'SKIPPED') {
|
|
185
|
+
return {
|
|
186
|
+
status: 'warning',
|
|
187
|
+
message: 'Claude Code credentials not found on host - creating this sandbox WITHOUT Claude Code credentials.\n'
|
|
188
|
+
+ ' Claude Code is still installed in the image but will not be authenticated.\n'
|
|
189
|
+
+ ' To enable it: run "claude" once on the host to complete login, then re-run "ai sandbox create".'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { status: 'ready' };
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'fatal',
|
|
197
|
+
message: redactCommandError(error instanceof Error ? error.message : 'unknown credential error')
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const claudeCodeBeforeContainerCreateHook = {
|
|
203
|
+
id: 'claude-code-before-container-create',
|
|
204
|
+
phase: 'before-container-create',
|
|
205
|
+
run: async ({ create }) => {
|
|
206
|
+
const { hostHome, resolvedTools } = requireCreateContext(create, 'claude-code-before-container-create');
|
|
207
|
+
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
208
|
+
if (claudeCodeEntry) {
|
|
209
|
+
ensureClaudeOnboarding(claudeCodeEntry.dir, hostHome);
|
|
210
|
+
ensureClaudeSettings(claudeCodeEntry.dir, hostHome);
|
|
211
|
+
}
|
|
212
|
+
return { status: 'ready' };
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const claudeCodeBeforeEnterHook = {
|
|
216
|
+
id: 'claude-code-before-enter',
|
|
217
|
+
phase: 'before-enter',
|
|
218
|
+
run: async ({ enter }) => {
|
|
219
|
+
const { hostHome, hostEnv } = requireEnterContext(enter, 'claude-code-before-enter');
|
|
220
|
+
try {
|
|
221
|
+
validateClaudeCredentialsEnvOverride(hostEnv);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
status: 'fatal',
|
|
226
|
+
message: redactCommandError(error instanceof Error ? error.message : 'invalid credentials override')
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const providerAuthAvailable = hasClaudeProviderAuth(hostHome);
|
|
231
|
+
const result = reconcileClaudeCredentials(hostHome, { envFn: () => ({ ...hostEnv }) });
|
|
232
|
+
const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
|
|
233
|
+
return {
|
|
234
|
+
status: 'ready',
|
|
235
|
+
...(message === null ? {} : { message })
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
status: 'warning',
|
|
241
|
+
message: `Failed to sync Claude Code credentials: ${redactCommandError(error instanceof Error ? error.message : 'unknown error')}`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
export { claudeCodeBeforeContainerCreateHook, claudeCodeBeforeEnterHook, claudeCodeCredentialPreflightHook };
|
|
247
|
+
//# sourceMappingURL=claude-code-sandbox.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineAgentClientAdapter } from '../adapter.js';
|
|
2
2
|
import { hostJoin } from '../../sandbox/engines/wsl2-paths.js';
|
|
3
|
+
import { claudeCodeBeforeContainerCreateHook, claudeCodeBeforeEnterHook, claudeCodeCredentialPreflightHook } from './claude-code-sandbox.js';
|
|
3
4
|
const claudeCodeAdapter = defineAgentClientAdapter({
|
|
4
5
|
id: 'claude-code',
|
|
5
6
|
displayName: 'Claude Code',
|
|
@@ -25,7 +26,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
|
|
|
25
26
|
},
|
|
26
27
|
project: {
|
|
27
28
|
ownedPathPrefixes: ['.claude/'],
|
|
28
|
-
managed: ['.claude/commands/', '.claude/agents/'],
|
|
29
|
+
managed: ['.claude/commands/', '.claude/agents/', '.claude/rules/'],
|
|
29
30
|
merged: ['.claude/settings.json'],
|
|
30
31
|
ejected: [],
|
|
31
32
|
seedCommands: [{
|
|
@@ -34,9 +35,18 @@ const claudeCodeAdapter = defineAgentClientAdapter({
|
|
|
34
35
|
'zh-CN': '.claude/commands/update-agent-infra.zh-CN.md'
|
|
35
36
|
},
|
|
36
37
|
target: '.claude/commands/update-agent-infra.md'
|
|
37
|
-
}]
|
|
38
|
+
}],
|
|
39
|
+
customCommand: {
|
|
40
|
+
target: '.claude/commands/${skillName}.md',
|
|
41
|
+
frontmatter: {},
|
|
42
|
+
includeUsage: true,
|
|
43
|
+
inheritDisableModelInvocation: true
|
|
44
|
+
}
|
|
38
45
|
},
|
|
39
46
|
sandbox: {
|
|
47
|
+
image: {
|
|
48
|
+
dockerfileFragment: 'claude-code'
|
|
49
|
+
},
|
|
40
50
|
createTool: ({ home, project }) => ({
|
|
41
51
|
id: 'claude-code',
|
|
42
52
|
name: 'Claude Code',
|
|
@@ -64,7 +74,11 @@ const claudeCodeAdapter = defineAgentClientAdapter({
|
|
|
64
74
|
{ name: 'claude-yolo', command: 'claude --dangerously-skip-permissions; tput ed' },
|
|
65
75
|
{ name: 'cy', command: 'claude --dangerously-skip-permissions; tput ed' }
|
|
66
76
|
],
|
|
67
|
-
hooks: [
|
|
77
|
+
hooks: [
|
|
78
|
+
claudeCodeCredentialPreflightHook,
|
|
79
|
+
claudeCodeBeforeContainerCreateHook,
|
|
80
|
+
claudeCodeBeforeEnterHook
|
|
81
|
+
]
|
|
68
82
|
}
|
|
69
83
|
});
|
|
70
84
|
export { claudeCodeAdapter };
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
const LIFECYCLE_HOOKS = Object.freeze([
|
|
8
|
+
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
9
|
+
Object.freeze({ event: 'PostToolUse', matcher: '^collaborationspawn_agent$', phase: 'post-tool' }),
|
|
10
|
+
Object.freeze({ event: 'SubagentStart', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-start' }),
|
|
11
|
+
Object.freeze({ event: 'SubagentStop', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-stop' })
|
|
12
|
+
]);
|
|
13
|
+
function object(value) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
function nonEmpty(value) {
|
|
19
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
20
|
+
}
|
|
21
|
+
function validateCodexLifecycleHookConfig(value) {
|
|
22
|
+
const hooks = object(object(value)?.hooks);
|
|
23
|
+
const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
|
|
24
|
+
const entries = hooks[event];
|
|
25
|
+
if (!Array.isArray(entries))
|
|
26
|
+
return false;
|
|
27
|
+
return entries.some((entryValue) => {
|
|
28
|
+
const entry = object(entryValue);
|
|
29
|
+
if (entry?.matcher !== matcher || !Array.isArray(entry.hooks))
|
|
30
|
+
return false;
|
|
31
|
+
return entry.hooks.some((hookValue) => {
|
|
32
|
+
const hook = object(hookValue);
|
|
33
|
+
return hook?.type === 'command'
|
|
34
|
+
&& hook.timeout === 5
|
|
35
|
+
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
if (!valid)
|
|
40
|
+
throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
|
|
41
|
+
}
|
|
42
|
+
function parseCodexHooksList(value, repoRoot) {
|
|
43
|
+
const root = object(value);
|
|
44
|
+
const entries = Array.isArray(root?.data) ? root.data : [];
|
|
45
|
+
const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
|
|
46
|
+
const errors = Array.isArray(entry?.errors) ? entry.errors : [];
|
|
47
|
+
if (errors.length)
|
|
48
|
+
throw new Error(`CODEX_PREFLIGHT_HOOK_DISCOVERY_FAILED: ${errors.map(String).join('; ')}`);
|
|
49
|
+
const hooks = (Array.isArray(entry?.hooks) ? entry.hooks : [])
|
|
50
|
+
.map(object)
|
|
51
|
+
.filter((hook) => hook !== null)
|
|
52
|
+
.map((hook) => Object.freeze({
|
|
53
|
+
eventName: String(hook.eventName ?? ''),
|
|
54
|
+
matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
|
|
55
|
+
command: String(hook.command ?? ''),
|
|
56
|
+
enabled: hook.enabled === true
|
|
57
|
+
}));
|
|
58
|
+
const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) => hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
|
|
59
|
+
&& hook.matcher === matcher
|
|
60
|
+
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
|
|
61
|
+
&& hook.enabled));
|
|
62
|
+
if (!valid)
|
|
63
|
+
throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
|
|
64
|
+
return Object.freeze(hooks);
|
|
65
|
+
}
|
|
66
|
+
function hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, identity) {
|
|
67
|
+
if (!identity || !fs.existsSync(runtimeRoot))
|
|
68
|
+
return false;
|
|
69
|
+
return fs.readdirSync(runtimeRoot)
|
|
70
|
+
.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
|
|
71
|
+
.some((name) => {
|
|
72
|
+
try {
|
|
73
|
+
const record = object(JSON.parse(fs.readFileSync(path.join(runtimeRoot, name), 'utf8')));
|
|
74
|
+
const state = object(record?.state);
|
|
75
|
+
const spawnEvent = object(state?.spawn);
|
|
76
|
+
return !['invalid', 'expired'].includes(String(state?.status))
|
|
77
|
+
&& spawnEvent?.hookDefinitionHash === hookDefinitionHash
|
|
78
|
+
&& spawnEvent.sessionId === identity.sessionId
|
|
79
|
+
&& spawnEvent.turnId === identity.turnId
|
|
80
|
+
&& spawnEvent.toolUseId === identity.toolUseId;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function parseCodexThreadResolution(readValue, resumeValue) {
|
|
88
|
+
const readRoot = object(readValue);
|
|
89
|
+
const resumeRoot = object(resumeValue);
|
|
90
|
+
const thread = object(readRoot?.thread);
|
|
91
|
+
const resumedThread = object(resumeRoot?.thread);
|
|
92
|
+
const source = object(thread?.source);
|
|
93
|
+
const subAgent = object(source?.subAgent);
|
|
94
|
+
const threadSpawn = object(subAgent?.thread_spawn);
|
|
95
|
+
const childThreadId = nonEmpty(thread?.id);
|
|
96
|
+
const parentThreadId = nonEmpty(thread?.parentThreadId);
|
|
97
|
+
const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
|
|
98
|
+
const resumedChildThreadId = nonEmpty(resumedThread?.id);
|
|
99
|
+
const model = nonEmpty(resumeRoot?.model);
|
|
100
|
+
const reasoningEffort = nonEmpty(resumeRoot?.reasoningEffort ?? object(resumeRoot?.settings)?.effort);
|
|
101
|
+
const forkedFromId = thread?.forkedFromId;
|
|
102
|
+
if (!childThreadId
|
|
103
|
+
|| resumedChildThreadId !== childThreadId
|
|
104
|
+
|| !parentThreadId
|
|
105
|
+
|| !sourceParentThreadId
|
|
106
|
+
|| !model
|
|
107
|
+
|| !reasoningEffort
|
|
108
|
+
|| (forkedFromId !== null && typeof forkedFromId !== 'string')) {
|
|
109
|
+
throw new Error('Codex App Server thread resolution is invalid');
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
thread: Object.freeze({
|
|
113
|
+
type: 'app-thread',
|
|
114
|
+
childThreadId,
|
|
115
|
+
parentThreadId,
|
|
116
|
+
forkedFromId,
|
|
117
|
+
sourceParentThreadId
|
|
118
|
+
}),
|
|
119
|
+
settings: Object.freeze({
|
|
120
|
+
type: 'app-settings',
|
|
121
|
+
childThreadId,
|
|
122
|
+
model,
|
|
123
|
+
reasoningEffort
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function parseCodexTurnCompleted(value) {
|
|
128
|
+
const root = object(value);
|
|
129
|
+
const turn = object(root?.turn);
|
|
130
|
+
const childThreadId = nonEmpty(root?.threadId);
|
|
131
|
+
const turnId = nonEmpty(turn?.id);
|
|
132
|
+
const status = turn?.status;
|
|
133
|
+
if (!childThreadId || !turnId || !['completed', 'interrupted', 'failed'].includes(String(status))) {
|
|
134
|
+
throw new Error('Codex App Server turn completion is invalid');
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
type: 'app-terminal',
|
|
138
|
+
childThreadId,
|
|
139
|
+
turnId,
|
|
140
|
+
status: status
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function parseCodexModelReroute(value) {
|
|
144
|
+
const root = object(value);
|
|
145
|
+
const childThreadId = nonEmpty(root?.threadId);
|
|
146
|
+
const turnId = nonEmpty(root?.turnId);
|
|
147
|
+
const fromModel = nonEmpty(root?.fromModel);
|
|
148
|
+
const toModel = nonEmpty(root?.toModel);
|
|
149
|
+
const reasonValue = root?.reason;
|
|
150
|
+
const reason = nonEmpty(reasonValue) ?? (object(reasonValue) ? JSON.stringify(reasonValue) : null);
|
|
151
|
+
if (!childThreadId || !turnId || !fromModel || !toModel || !reason) {
|
|
152
|
+
throw new Error('Codex App Server model reroute is invalid');
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze({ type: 'app-reroute', childThreadId, turnId, fromModel, toModel, reason });
|
|
155
|
+
}
|
|
156
|
+
class CodexAppServerTransport {
|
|
157
|
+
#options;
|
|
158
|
+
#child = null;
|
|
159
|
+
#buffer = '';
|
|
160
|
+
#stderr = '';
|
|
161
|
+
#nextId = 1;
|
|
162
|
+
#pending = new Map();
|
|
163
|
+
#notifications = [];
|
|
164
|
+
constructor(options = {}) {
|
|
165
|
+
this.#options = {
|
|
166
|
+
...options,
|
|
167
|
+
command: options.command ?? 'codex',
|
|
168
|
+
args: options.args ?? ['app-server', '--stdio'],
|
|
169
|
+
timeoutMs: options.timeoutMs ?? 5_000
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
get diagnostics() {
|
|
173
|
+
return Object.freeze(this.#stderr.split(/\r?\n/).filter(Boolean));
|
|
174
|
+
}
|
|
175
|
+
get notifications() {
|
|
176
|
+
return Object.freeze([...this.#notifications]);
|
|
177
|
+
}
|
|
178
|
+
start() {
|
|
179
|
+
if (this.#child)
|
|
180
|
+
throw new Error('Codex App Server transport is already started');
|
|
181
|
+
const child = spawn(this.#options.command, [...this.#options.args], {
|
|
182
|
+
cwd: this.#options.cwd,
|
|
183
|
+
env: this.#options.env,
|
|
184
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
185
|
+
});
|
|
186
|
+
this.#child = child;
|
|
187
|
+
child.stdout.setEncoding('utf8');
|
|
188
|
+
child.stderr.setEncoding('utf8');
|
|
189
|
+
child.stdout.on('data', (chunk) => this.#onData(chunk));
|
|
190
|
+
child.stderr.on('data', (chunk) => {
|
|
191
|
+
this.#stderr = `${this.#stderr}${chunk}`.slice(-32 * 1024);
|
|
192
|
+
});
|
|
193
|
+
child.on('error', (error) => this.#rejectAll(error));
|
|
194
|
+
child.on('exit', (code, signal) => {
|
|
195
|
+
if (this.#pending.size) {
|
|
196
|
+
this.#rejectAll(new Error(`Codex App Server exited before responding (code=${code}, signal=${signal})`));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
#onData(chunk) {
|
|
201
|
+
this.#buffer += chunk;
|
|
202
|
+
for (;;) {
|
|
203
|
+
const newline = this.#buffer.indexOf('\n');
|
|
204
|
+
if (newline < 0)
|
|
205
|
+
return;
|
|
206
|
+
const line = this.#buffer.slice(0, newline).trim();
|
|
207
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
208
|
+
if (!line)
|
|
209
|
+
continue;
|
|
210
|
+
let message;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = object(JSON.parse(line));
|
|
213
|
+
if (!parsed)
|
|
214
|
+
throw new Error('message is not an object');
|
|
215
|
+
message = parsed;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.#rejectAll(new Error(`Codex App Server returned invalid JSON: ${String(error)}`));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (typeof message.id === 'number') {
|
|
222
|
+
const pending = this.#pending.get(message.id);
|
|
223
|
+
if (!pending)
|
|
224
|
+
continue;
|
|
225
|
+
clearTimeout(pending.timer);
|
|
226
|
+
this.#pending.delete(message.id);
|
|
227
|
+
if (message.error)
|
|
228
|
+
pending.reject(new Error(`Codex App Server request failed: ${JSON.stringify(message.error)}`));
|
|
229
|
+
else
|
|
230
|
+
pending.resolve(message.result);
|
|
231
|
+
}
|
|
232
|
+
else if (typeof message.method === 'string') {
|
|
233
|
+
this.#notifications.push(Object.freeze({ method: message.method, params: message.params }));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
#rejectAll(error) {
|
|
238
|
+
for (const [id, pending] of this.#pending) {
|
|
239
|
+
clearTimeout(pending.timer);
|
|
240
|
+
this.#pending.delete(id);
|
|
241
|
+
pending.reject(error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
request(method, params) {
|
|
245
|
+
if (!this.#child || !this.#child.stdin.writable) {
|
|
246
|
+
return Promise.reject(new Error('Codex App Server transport is not running'));
|
|
247
|
+
}
|
|
248
|
+
const id = this.#nextId++;
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const timer = setTimeout(() => {
|
|
251
|
+
this.#pending.delete(id);
|
|
252
|
+
reject(new Error(`Codex App Server request '${method}' timed out`));
|
|
253
|
+
}, this.#options.timeoutMs);
|
|
254
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
255
|
+
this.#child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
notify(method, params = {}) {
|
|
259
|
+
if (!this.#child || !this.#child.stdin.writable)
|
|
260
|
+
throw new Error('Codex App Server transport is not running');
|
|
261
|
+
this.#child.stdin.write(`${JSON.stringify({ method, params })}\n`);
|
|
262
|
+
}
|
|
263
|
+
async initialize() {
|
|
264
|
+
const result = await this.request('initialize', {
|
|
265
|
+
clientInfo: { name: 'agent-infra', title: 'agent-infra', version: '0.9.5' },
|
|
266
|
+
capabilities: null
|
|
267
|
+
});
|
|
268
|
+
this.notify('initialized');
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
close() {
|
|
272
|
+
if (!this.#child)
|
|
273
|
+
return;
|
|
274
|
+
this.#child.stdin.end();
|
|
275
|
+
this.#child.kill();
|
|
276
|
+
this.#child = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function resolveCodexThread(childThreadId, options = {}) {
|
|
280
|
+
if (!nonEmpty(childThreadId))
|
|
281
|
+
throw new Error('Codex child thread id is required');
|
|
282
|
+
const transport = new CodexAppServerTransport(options);
|
|
283
|
+
transport.start();
|
|
284
|
+
try {
|
|
285
|
+
await transport.initialize();
|
|
286
|
+
const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
287
|
+
const readThread = object(readResult?.thread);
|
|
288
|
+
if (nonEmpty(readThread?.id) !== childThreadId)
|
|
289
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
290
|
+
const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
|
|
291
|
+
let resolution;
|
|
292
|
+
try {
|
|
293
|
+
resolution = parseCodexThreadResolution(readResult, resumeResult);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
throw new Error('Codex App Server resumed settings are unavailable');
|
|
297
|
+
}
|
|
298
|
+
const reroutes = transport.notifications
|
|
299
|
+
.filter((entry) => entry.method === 'model/rerouted')
|
|
300
|
+
.map((entry) => parseCodexModelReroute(entry.params))
|
|
301
|
+
.filter((event) => event.childThreadId === childThreadId);
|
|
302
|
+
return Object.freeze({ resolution, reroutes: Object.freeze(reroutes), diagnostics: transport.diagnostics });
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
try {
|
|
306
|
+
await transport.request('thread/unsubscribe', { threadId: childThreadId });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Closing the short-lived transport still removes the subscription.
|
|
310
|
+
}
|
|
311
|
+
transport.close();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function resolveCodexTerminal(childThreadId, options = {}) {
|
|
315
|
+
const transport = new CodexAppServerTransport(options);
|
|
316
|
+
transport.start();
|
|
317
|
+
try {
|
|
318
|
+
await transport.initialize();
|
|
319
|
+
const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
|
|
320
|
+
const thread = object(result?.thread);
|
|
321
|
+
if (nonEmpty(thread?.id) !== childThreadId)
|
|
322
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
323
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
324
|
+
const turn = turns.at(-1);
|
|
325
|
+
return parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
transport.close();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtimeIdentity) {
|
|
332
|
+
const versionRun = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
333
|
+
const match = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(versionRun.stdout ?? '');
|
|
334
|
+
if (versionRun.status !== 0 || !match?.[1] || !semver.gte(match[1], '0.147.0')) {
|
|
335
|
+
throw new Error('CODEX_PREFLIGHT_VERSION_UNSUPPORTED');
|
|
336
|
+
}
|
|
337
|
+
const featuresRun = spawnSync('codex', ['features', 'list'], { encoding: 'utf8' });
|
|
338
|
+
if (featuresRun.status !== 0
|
|
339
|
+
|| !/^hooks\s+\S+\s+true$/m.test(featuresRun.stdout)
|
|
340
|
+
|| !/^multi_agent\s+\S+\s+true$/m.test(featuresRun.stdout))
|
|
341
|
+
throw new Error('CODEX_PREFLIGHT_FEATURE_DISABLED');
|
|
342
|
+
const hooksPath = path.join(repoRoot, '.codex', 'hooks.json');
|
|
343
|
+
const hooksRaw = fs.readFileSync(hooksPath, 'utf8');
|
|
344
|
+
validateCodexLifecycleHookConfig(JSON.parse(hooksRaw));
|
|
345
|
+
const schemaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-codex-schema-'));
|
|
346
|
+
try {
|
|
347
|
+
const schemaRun = spawnSync('codex', ['app-server', 'generate-json-schema', '--out', schemaDir], { encoding: 'utf8' });
|
|
348
|
+
if (schemaRun.status !== 0)
|
|
349
|
+
throw new Error('CODEX_PREFLIGHT_SCHEMA_UNAVAILABLE');
|
|
350
|
+
const schema = fs.readFileSync(path.join(schemaDir, 'codex_app_server_protocol.v2.schemas.json'), 'utf8');
|
|
351
|
+
for (const field of ['parentThreadId', 'forkedFromId', 'ThreadSettings', 'turn/completed', 'model/rerouted']) {
|
|
352
|
+
if (!schema.includes(field))
|
|
353
|
+
throw new Error(`CODEX_PREFLIGHT_SCHEMA_MISSING:${field}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
358
|
+
}
|
|
359
|
+
const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
|
|
360
|
+
const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
|
|
361
|
+
const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
|
|
362
|
+
const transport = new CodexAppServerTransport();
|
|
363
|
+
transport.start();
|
|
364
|
+
let discoveredHooks;
|
|
365
|
+
try {
|
|
366
|
+
await transport.initialize();
|
|
367
|
+
discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
transport.close();
|
|
371
|
+
}
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
cliVersion: match[1],
|
|
374
|
+
hookDefinitionHash,
|
|
375
|
+
staticReady: true,
|
|
376
|
+
discoveredHooks,
|
|
377
|
+
runtimeLiveness,
|
|
378
|
+
diagnostics: transport.diagnostics
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
|
|
382
|
+
//# sourceMappingURL=app-server.js.map
|