@fitlab-ai/agent-infra 0.8.0 → 0.8.2
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/cli.ts +4 -4
- package/dist/bin/cli.js +4 -4
- package/dist/lib/defaults.json +1 -0
- package/dist/lib/run/index.js +96 -5
- package/dist/lib/sandbox/capture.js +113 -4
- package/dist/lib/sandbox/commands/create.js +37 -4
- package/dist/lib/sandbox/commands/enter.js +7 -3
- package/dist/lib/sandbox/config.js +1 -1
- package/dist/lib/sandbox/credentials.js +42 -3
- package/dist/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/dist/lib/sandbox/tools.js +9 -0
- package/dist/lib/server/adapters/feishu/index.js +5 -3
- package/dist/lib/server/adapters/feishu/transport.js +20 -2
- package/dist/lib/server/protocol.js +20 -4
- package/dist/lib/task/commands/log.js +36 -14
- package/dist/lib/task/commands/status.js +144 -45
- package/dist/lib/update.js +31 -0
- package/lib/defaults.json +1 -0
- package/lib/run/index.ts +119 -6
- package/lib/sandbox/capture.ts +136 -5
- package/lib/sandbox/commands/create.ts +42 -4
- package/lib/sandbox/commands/enter.ts +8 -2
- package/lib/sandbox/config.ts +1 -1
- package/lib/sandbox/credentials.ts +57 -3
- package/lib/sandbox/runtimes/base.dockerfile +13 -0
- package/lib/sandbox/runtimes/node20.dockerfile +1 -1
- package/lib/sandbox/runtimes/node22.dockerfile +1 -1
- package/lib/sandbox/tools.ts +9 -0
- package/lib/server/adapters/feishu/index.ts +5 -3
- package/lib/server/adapters/feishu/transport.ts +32 -3
- package/lib/server/protocol.ts +20 -4
- package/lib/task/commands/log.ts +36 -14
- package/lib/task/commands/status.ts +200 -58
- package/lib/update.ts +34 -0
- package/package.json +5 -4
- package/templates/.agents/README.en.md +3 -3
- package/templates/.agents/README.zh-CN.md +3 -3
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +3 -3
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -3
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -3
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +16 -16
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +11 -11
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +11 -11
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +4 -4
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +5 -5
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +26 -1
package/lib/sandbox/capture.ts
CHANGED
|
@@ -22,22 +22,45 @@ export type SandboxCaptureResult = {
|
|
|
22
22
|
signal: NodeJS.Signals | null;
|
|
23
23
|
stdout: string;
|
|
24
24
|
stderr: string;
|
|
25
|
+
run?: SandboxRunMetadata;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SandboxRunMetadata = {
|
|
29
|
+
runId: string;
|
|
30
|
+
engine: string;
|
|
31
|
+
container: string;
|
|
32
|
+
runDir: string;
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
export type SandboxCaptureOptions = {
|
|
28
36
|
engine?: string;
|
|
29
37
|
repoRoot?: string;
|
|
38
|
+
runId?: string;
|
|
30
39
|
containerCandidates?: string[];
|
|
31
40
|
rows?: SandboxRow[];
|
|
32
41
|
startContainer?: (name: string) => void;
|
|
33
42
|
spawn?: (file: string, args: string[]) => Promise<SandboxCaptureResult>;
|
|
34
43
|
};
|
|
35
44
|
|
|
36
|
-
async function spawnCapture(
|
|
45
|
+
async function spawnCapture(
|
|
46
|
+
file: string,
|
|
47
|
+
args: string[]
|
|
48
|
+
): Promise<SandboxCaptureResult> {
|
|
37
49
|
return new Promise((resolve, reject) => {
|
|
38
50
|
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
51
|
let stdout = '';
|
|
40
52
|
let stderr = '';
|
|
53
|
+
let settled = false;
|
|
54
|
+
const rejectOnce = (error: unknown): void => {
|
|
55
|
+
if (settled) return;
|
|
56
|
+
settled = true;
|
|
57
|
+
reject(error);
|
|
58
|
+
};
|
|
59
|
+
const resolveOnce = (result: SandboxCaptureResult): void => {
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
resolve(result);
|
|
63
|
+
};
|
|
41
64
|
child.stdout.setEncoding('utf8');
|
|
42
65
|
child.stderr.setEncoding('utf8');
|
|
43
66
|
child.stdout.on('data', (chunk) => {
|
|
@@ -46,11 +69,106 @@ async function spawnCapture(file: string, args: string[]): Promise<SandboxCaptur
|
|
|
46
69
|
child.stderr.on('data', (chunk) => {
|
|
47
70
|
stderr += chunk;
|
|
48
71
|
});
|
|
49
|
-
child.on('error',
|
|
50
|
-
child.on('close', (exitCode, signal) =>
|
|
72
|
+
child.on('error', rejectOnce);
|
|
73
|
+
child.on('close', (exitCode, signal) => {
|
|
74
|
+
resolveOnce({ exitCode, signal, stdout, stderr });
|
|
75
|
+
});
|
|
51
76
|
});
|
|
52
77
|
}
|
|
53
78
|
|
|
79
|
+
function shellQuote(value: string): string {
|
|
80
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createRunId(date: Date = new Date()): string {
|
|
84
|
+
return `run-${date.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '').replace('T', '-').replace('Z', '')}-${process.pid}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildRunScript(params: {
|
|
88
|
+
command: string[];
|
|
89
|
+
runDir: string;
|
|
90
|
+
runId: string;
|
|
91
|
+
}): string {
|
|
92
|
+
const command = params.command.map(shellQuote).join(' ');
|
|
93
|
+
const runDir = shellQuote(params.runDir);
|
|
94
|
+
const runId = shellQuote(params.runId);
|
|
95
|
+
return `#!/bin/sh
|
|
96
|
+
set -u
|
|
97
|
+
cd /workspace
|
|
98
|
+
date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/started_at
|
|
99
|
+
printf '%s\\n' running > ${runDir}/status
|
|
100
|
+
${command}
|
|
101
|
+
code=$?
|
|
102
|
+
printf '%s\\n' "$code" > ${runDir}/exit_code
|
|
103
|
+
date "+%Y-%m-%d %H:%M:%S%:z" > ${runDir}/finished_at
|
|
104
|
+
if [ "$code" -eq 0 ]; then
|
|
105
|
+
printf '%s\\n' completed > ${runDir}/status
|
|
106
|
+
else
|
|
107
|
+
printf '%s\\n' failed > ${runDir}/status
|
|
108
|
+
fi
|
|
109
|
+
printf '\\n[agent-infra] run %s finished with exit code %s\\n' ${runId} "$code"
|
|
110
|
+
exec bash -l
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildTmuxLauncher(params: {
|
|
115
|
+
request: SandboxCaptureRequest;
|
|
116
|
+
runId: string;
|
|
117
|
+
}): string {
|
|
118
|
+
const session = 'work';
|
|
119
|
+
const window = `ai-${params.runId.replace(/^run-/, '').slice(0, 18)}`;
|
|
120
|
+
const runRoot = '/tmp/agent-infra-runs';
|
|
121
|
+
const runDir = `${runRoot}/${params.runId}`;
|
|
122
|
+
const runScript = buildRunScript({ command: params.request.command, runDir, runId: params.runId });
|
|
123
|
+
const runScriptBase64 = Buffer.from(runScript, 'utf8').toString('base64');
|
|
124
|
+
const paneCommand = `cd /workspace && ${shellQuote(`${runDir}/run.sh`)}`;
|
|
125
|
+
|
|
126
|
+
return `set -eu
|
|
127
|
+
session=${shellQuote(session)}
|
|
128
|
+
window=${shellQuote(window)}
|
|
129
|
+
run_id=${shellQuote(params.runId)}
|
|
130
|
+
run_dir=${shellQuote(runDir)}
|
|
131
|
+
task_ref=${shellQuote(params.request.taskRef)}
|
|
132
|
+
branch=${shellQuote(params.request.branch)}
|
|
133
|
+
|
|
134
|
+
sandbox-dotfiles-link >/dev/null 2>&1 || true
|
|
135
|
+
mkdir -p "$run_dir"
|
|
136
|
+
printf '%s\\n' pending > "$run_dir/status"
|
|
137
|
+
printf '%s\\n' "$task_ref" > "$run_dir/task_ref"
|
|
138
|
+
printf '%s\\n' "$branch" > "$run_dir/branch"
|
|
139
|
+
printf '%s\\n' ${shellQuote(params.request.command.join(' '))} > "$run_dir/command"
|
|
140
|
+
printf '%s' ${shellQuote(runScriptBase64)} | base64 -d > "$run_dir/run.sh"
|
|
141
|
+
chmod +x "$run_dir/run.sh"
|
|
142
|
+
|
|
143
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
144
|
+
printf '%s\\n' "tmux is not installed in this sandbox" >&2
|
|
145
|
+
exit 127
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
if ! tmux has-session -t "$session" 2>/dev/null; then
|
|
149
|
+
tmux new-session -d -s "$session" -n shell
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
if [ -n "\${TZ:-}" ]; then
|
|
153
|
+
tmux set-environment -t "$session" TZ "$TZ" 2>/dev/null || true
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
pane=$(tmux new-window -d -P -F '#{pane_id}' -t "$session" -n "$window")
|
|
157
|
+
printf '%s\\n' "$session" > "$run_dir/session"
|
|
158
|
+
printf '%s\\n' "$window" > "$run_dir/window"
|
|
159
|
+
printf '%s\\n' "$pane" > "$run_dir/pane"
|
|
160
|
+
tmux pipe-pane -o -t "$pane" "cat > $run_dir/output.log"
|
|
161
|
+
tmux send-keys -t "$pane" ${shellQuote(paneCommand)} Enter
|
|
162
|
+
|
|
163
|
+
cat <<EOF
|
|
164
|
+
Started sandbox run $run_id in tmux session '$session', window '$window', pane '$pane'.
|
|
165
|
+
Attach with: ai sandbox enter $task_ref
|
|
166
|
+
Status file: $run_dir/status
|
|
167
|
+
Output log: $run_dir/output.log
|
|
168
|
+
EOF
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
54
172
|
export async function runInSandbox(
|
|
55
173
|
request: SandboxCaptureRequest,
|
|
56
174
|
options: SandboxCaptureOptions = {}
|
|
@@ -73,12 +191,25 @@ export async function runInSandbox(
|
|
|
73
191
|
if (!found.running) {
|
|
74
192
|
(options.startContainer ?? ((name: string) => startSandboxContainer(engine, name)))(found.name);
|
|
75
193
|
}
|
|
194
|
+
const runId = options.runId ?? createRunId();
|
|
195
|
+
const runDir = `/tmp/agent-infra-runs/${runId}`;
|
|
76
196
|
const dockerArgs = [
|
|
77
197
|
'exec',
|
|
78
198
|
...terminalEnvFlags(),
|
|
79
199
|
...hostTimezoneEnvFlags(),
|
|
80
200
|
found.name,
|
|
81
|
-
|
|
201
|
+
'bash',
|
|
202
|
+
'-lc',
|
|
203
|
+
buildTmuxLauncher({ request, runId })
|
|
82
204
|
];
|
|
83
|
-
|
|
205
|
+
const result = await (options.spawn ?? spawnCapture)('docker', dockerArgs);
|
|
206
|
+
return {
|
|
207
|
+
...result,
|
|
208
|
+
run: {
|
|
209
|
+
runId,
|
|
210
|
+
engine,
|
|
211
|
+
container: found.name,
|
|
212
|
+
runDir
|
|
213
|
+
}
|
|
214
|
+
};
|
|
84
215
|
}
|
|
@@ -915,6 +915,42 @@ function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | n
|
|
|
915
915
|
}
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
+
const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'] as const;
|
|
919
|
+
|
|
920
|
+
function removeCodexMcpServers(sandboxParsed: JsonObject): boolean {
|
|
921
|
+
if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
delete sandboxParsed.mcp_servers;
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function inheritDisabledCodexFeatureFlags(sandboxParsed: JsonObject, hostParsed: JsonObject): boolean {
|
|
929
|
+
if (!isJsonObjectRecord(hostParsed.features)) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let sandboxFeatures = sandboxParsed.features as JsonObject | undefined;
|
|
937
|
+
let changed = false;
|
|
938
|
+
for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
|
|
939
|
+
if (hostParsed.features[key] !== false) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (!sandboxFeatures) {
|
|
943
|
+
sandboxFeatures = {};
|
|
944
|
+
sandboxParsed.features = sandboxFeatures;
|
|
945
|
+
}
|
|
946
|
+
if (sandboxFeatures[key] !== false) {
|
|
947
|
+
sandboxFeatures[key] = false;
|
|
948
|
+
changed = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return changed;
|
|
952
|
+
}
|
|
953
|
+
|
|
918
954
|
export function ensureCodexModelInheritance(
|
|
919
955
|
toolDir: string,
|
|
920
956
|
hostHomeDir?: string,
|
|
@@ -947,13 +983,15 @@ export function ensureCodexModelInheritance(
|
|
|
947
983
|
}
|
|
948
984
|
}
|
|
949
985
|
|
|
986
|
+
let changed = removeCodexMcpServers(sandboxParsed);
|
|
987
|
+
changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
|
|
988
|
+
|
|
950
989
|
const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
|
|
951
990
|
['model', 'string'],
|
|
952
991
|
['model_reasoning_effort', 'string'],
|
|
953
992
|
['model_auto_compact_token_limit', 'number']
|
|
954
993
|
];
|
|
955
994
|
|
|
956
|
-
let changed = false;
|
|
957
995
|
for (const [key, type] of inheritSpecs) {
|
|
958
996
|
if (Object.hasOwn(sandboxParsed, key)) {
|
|
959
997
|
continue;
|
|
@@ -1438,9 +1476,9 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1438
1476
|
if (claudeCodeEntry) {
|
|
1439
1477
|
ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1440
1478
|
ensureClaudeSettings(claudeCodeEntry.dir, effectiveConfig.home);
|
|
1441
|
-
// prepareClaudeCredentials wrote
|
|
1442
|
-
//
|
|
1443
|
-
// claude-code entry was removed
|
|
1479
|
+
// prepareClaudeCredentials wrote OAuth credentials or confirmed
|
|
1480
|
+
// provider/API settings are enough for Claude Code. If no usable
|
|
1481
|
+
// auth was present, the claude-code entry was removed above.
|
|
1444
1482
|
}
|
|
1445
1483
|
const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
|
|
1446
1484
|
if (codexEntry) {
|
|
@@ -9,6 +9,7 @@ import { detectEngine } from '../engine.ts';
|
|
|
9
9
|
import {
|
|
10
10
|
formatCredentialWarnings,
|
|
11
11
|
formatRemaining,
|
|
12
|
+
hasClaudeProviderAuth,
|
|
12
13
|
reconcileClaudeCredentials,
|
|
13
14
|
redactCommandError,
|
|
14
15
|
validateClaudeCredentialsEnvOverride
|
|
@@ -93,8 +94,12 @@ export function runSandboxInteractive(params: {
|
|
|
93
94
|
|
|
94
95
|
export function formatCredentialSyncStatus(
|
|
95
96
|
result: ReturnType<typeof reconcileClaudeCredentials>,
|
|
96
|
-
isTTY = process.stderr.isTTY
|
|
97
|
+
isTTY = process.stderr.isTTY,
|
|
98
|
+
providerAuthAvailable = false
|
|
97
99
|
): string | null {
|
|
100
|
+
if (providerAuthAvailable && (result.status === 'STALE_ACCESS' || result.status === 'MISSING')) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
98
103
|
if (result.status === 'STALE_ACCESS') {
|
|
99
104
|
return 'Warning: Claude Code credentials on host appear stale. Run "ai sandbox refresh" or "claude /login" to renew.\n';
|
|
100
105
|
}
|
|
@@ -158,8 +163,9 @@ export async function enter(args: string[]): Promise<number> {
|
|
|
158
163
|
if (config.tools.includes('claude-code')) {
|
|
159
164
|
try {
|
|
160
165
|
// Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
|
|
166
|
+
const providerAuthAvailable = hasClaudeProviderAuth(config.home);
|
|
161
167
|
const result = reconcileClaudeCredentials(config.home);
|
|
162
|
-
const message = formatCredentialSyncStatus(result);
|
|
168
|
+
const message = formatCredentialSyncStatus(result, process.stderr.isTTY, providerAuthAvailable);
|
|
163
169
|
if (message) {
|
|
164
170
|
process.stderr.write(message);
|
|
165
171
|
}
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { SandboxTool } from './tools.ts';
|
|
|
12
12
|
const DEFAULTS = Object.freeze({
|
|
13
13
|
engine: null,
|
|
14
14
|
runtimes: ['node22'],
|
|
15
|
-
tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
15
|
+
tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
16
16
|
dockerfile: null,
|
|
17
17
|
vm: {
|
|
18
18
|
cpu: null,
|
|
@@ -55,6 +55,11 @@ type InspectOptions = {
|
|
|
55
55
|
envFn?: EnvFn;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
type ProviderAuthOptions = {
|
|
59
|
+
readFn?: ReadFn;
|
|
60
|
+
existsFn?: ExistsFn;
|
|
61
|
+
};
|
|
62
|
+
|
|
58
63
|
type WriteHostOptions = {
|
|
59
64
|
execFn?: ExecFn;
|
|
60
65
|
mkdirFn?: typeof fs.mkdirSync;
|
|
@@ -114,6 +119,14 @@ const REDACTION_PATTERNS = [
|
|
|
114
119
|
{ pattern: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, replacement: 'Bearer [REDACTED]' }
|
|
115
120
|
];
|
|
116
121
|
|
|
122
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
123
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasNonEmptyString(value: unknown): boolean {
|
|
127
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
117
130
|
function errorMessage(error: unknown): string {
|
|
118
131
|
return error instanceof Error ? error.message : 'unknown error';
|
|
119
132
|
}
|
|
@@ -219,6 +232,35 @@ export function validateClaudeCredentialsEnvOverride(env: NodeJS.ProcessEnv = pr
|
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
234
|
|
|
235
|
+
export function hasClaudeProviderAuthInSettings(settings: unknown): boolean {
|
|
236
|
+
if (!isRecord(settings)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const env = isRecord(settings.env) ? settings.env : {};
|
|
241
|
+
return hasNonEmptyString(env.ANTHROPIC_AUTH_TOKEN)
|
|
242
|
+
|| hasNonEmptyString(env.ANTHROPIC_API_KEY)
|
|
243
|
+
|| hasNonEmptyString(settings.apiKeyHelper);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function hasClaudeProviderAuth(home: string, options: ProviderAuthOptions = {}): boolean {
|
|
247
|
+
const {
|
|
248
|
+
readFn = (targetPath) => fs.readFileSync(targetPath, 'utf8'),
|
|
249
|
+
existsFn = fs.existsSync
|
|
250
|
+
} = options;
|
|
251
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
252
|
+
|
|
253
|
+
if (!existsFn(settingsPath)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
return hasClaudeProviderAuthInSettings(JSON.parse(readFn(settingsPath)));
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
222
264
|
// Reconcile treats the freshest valid endpoint as authoritative so sandbox
|
|
223
265
|
// token rotations can flow back to the host credential store.
|
|
224
266
|
function validateClaudeCredentialsBlob(raw: unknown, blob: string | null = null): CredentialInspection {
|
|
@@ -595,13 +637,22 @@ export function prepareClaudeCredentials(
|
|
|
595
637
|
resolvedTools: ResolvedTool[],
|
|
596
638
|
extractFn: (home: string) => string | null = extractClaudeCredentialsBlob,
|
|
597
639
|
writeFn: (home: string, project: string, blob: string) => void = writeClaudeCredentialsFile,
|
|
598
|
-
inspectFn: (home: string) => CredentialInspection = inspectClaudeKeychainStatus
|
|
640
|
+
inspectFn: (home: string) => CredentialInspection = inspectClaudeKeychainStatus,
|
|
641
|
+
providerAuthFn: (home: string) => boolean = hasClaudeProviderAuth
|
|
599
642
|
): ClaudeCredentialOutcome {
|
|
600
643
|
const claudeCodeEntry = resolvedTools.find(({ tool }) => tool.id === 'claude-code');
|
|
601
644
|
if (!claudeCodeEntry) {
|
|
602
645
|
return { status: 'NOT_APPLICABLE' };
|
|
603
646
|
}
|
|
604
647
|
|
|
648
|
+
const providerAuthAvailable = () => {
|
|
649
|
+
try {
|
|
650
|
+
return providerAuthFn(home);
|
|
651
|
+
} catch {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
605
656
|
let blob: string | null = null;
|
|
606
657
|
const hasCustomInspectFn = inspectFn !== inspectClaudeKeychainStatus;
|
|
607
658
|
const hasCustomExtractFn = extractFn !== extractClaudeCredentialsBlob;
|
|
@@ -612,7 +663,7 @@ export function prepareClaudeCredentials(
|
|
|
612
663
|
blob = inspection.blob;
|
|
613
664
|
break;
|
|
614
665
|
case 'MISSING':
|
|
615
|
-
return { status: 'SKIPPED' };
|
|
666
|
+
return providerAuthAvailable() ? { status: 'OK' } : { status: 'SKIPPED' };
|
|
616
667
|
case 'KEYCHAIN_LOCKED':
|
|
617
668
|
throw new Error([
|
|
618
669
|
'Claude Code credentials are stored in the macOS keychain, but the keychain is locked.',
|
|
@@ -620,6 +671,9 @@ export function prepareClaudeCredentials(
|
|
|
620
671
|
buildLockedGuidance()
|
|
621
672
|
].join('\n'));
|
|
622
673
|
case 'STALE_ACCESS':
|
|
674
|
+
if (providerAuthAvailable()) {
|
|
675
|
+
return { status: 'OK' };
|
|
676
|
+
}
|
|
623
677
|
throw new Error([
|
|
624
678
|
'Claude Code credentials on host are invalid or expired.',
|
|
625
679
|
'',
|
|
@@ -648,7 +702,7 @@ export function prepareClaudeCredentials(
|
|
|
648
702
|
} else {
|
|
649
703
|
blob = extractFn(home);
|
|
650
704
|
if (!blob) {
|
|
651
|
-
return { status: 'SKIPPED' };
|
|
705
|
+
return providerAuthAvailable() ? { status: 'OK' } : { status: 'SKIPPED' };
|
|
652
706
|
}
|
|
653
707
|
}
|
|
654
708
|
|
|
@@ -61,6 +61,19 @@ RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/c
|
|
|
61
61
|
#!/bin/sh
|
|
62
62
|
set -eu
|
|
63
63
|
|
|
64
|
+
SETTINGS_FILE="/home/devuser/.claude/settings.json"
|
|
65
|
+
if [ -r "$SETTINGS_FILE" ] && jq -e '
|
|
66
|
+
def has_nonempty($v):
|
|
67
|
+
if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
|
|
68
|
+
(if type == "object" then . else {} end) as $settings
|
|
69
|
+
| (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
|
|
70
|
+
| has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
|
|
71
|
+
or has_nonempty($env.ANTHROPIC_API_KEY)
|
|
72
|
+
or has_nonempty($settings.apiKeyHelper)
|
|
73
|
+
' "$SETTINGS_FILE" >/dev/null 2>&1; then
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
64
77
|
CRED_FILE="/home/devuser/.claude/.credentials.json"
|
|
65
78
|
[ -r "$CRED_FILE" ] || exit 0
|
|
66
79
|
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -124,6 +124,15 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
|
|
|
124
124
|
{ hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
|
|
125
125
|
{ hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
|
|
126
126
|
]
|
|
127
|
+
},
|
|
128
|
+
'agent-infra': {
|
|
129
|
+
id: 'agent-infra',
|
|
130
|
+
name: 'agent-infra CLI',
|
|
131
|
+
install: { type: 'npm', cmd: '@fitlab-ai/agent-infra@latest' },
|
|
132
|
+
sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'agent-infra'),
|
|
133
|
+
containerMount: '/home/devuser/.agent-infra-cli',
|
|
134
|
+
versionCmd: 'ai version --raw',
|
|
135
|
+
setupHint: 'Provides the ai and agent-infra CLI commands inside the sandbox.'
|
|
127
136
|
}
|
|
128
137
|
};
|
|
129
138
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Adapter, AdapterCtx, AdapterFactory, InboundMessage } from '../_contract.ts';
|
|
2
|
-
import { createFeishuTransport, normalizeMessage } from './transport.ts';
|
|
2
|
+
import { cardMessage, createFeishuTransport, normalizeMessage } from './transport.ts';
|
|
3
3
|
import type { FeishuTransport } from './transport.ts';
|
|
4
4
|
|
|
5
5
|
// Assemble the feishu adapter. The transport is injectable so unit tests can
|
|
@@ -25,7 +25,9 @@ export function createFeishuAdapter(
|
|
|
25
25
|
text: normalized.text,
|
|
26
26
|
messageId: normalized.messageId,
|
|
27
27
|
raw: normalized.raw,
|
|
28
|
-
reply: (text) =>
|
|
28
|
+
reply: async (text) => {
|
|
29
|
+
await transport.send(normalized.chatId, cardMessage(text));
|
|
30
|
+
}
|
|
29
31
|
};
|
|
30
32
|
await adapterCtx.dispatch(message);
|
|
31
33
|
} catch (error) {
|
|
@@ -37,7 +39,7 @@ export function createFeishuAdapter(
|
|
|
37
39
|
await transport.stop();
|
|
38
40
|
},
|
|
39
41
|
async sendMessage(target, text) {
|
|
40
|
-
await transport.send(target.chatId, text);
|
|
42
|
+
await transport.send(target.chatId, cardMessage(text));
|
|
41
43
|
}
|
|
42
44
|
};
|
|
43
45
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
3
|
|
|
3
4
|
// Transport layer for the feishu adapter. All @larksuiteoapi/node-sdk surface is
|
|
4
5
|
// confined here so the adapter body (index.ts) depends only on this narrow
|
|
@@ -10,7 +11,15 @@ export type FeishuTransport = {
|
|
|
10
11
|
// each inbound im.message.receive_v1.
|
|
11
12
|
start: (onMessage: (raw: unknown) => Promise<void>) => Promise<void>;
|
|
12
13
|
stop: () => Promise<void>;
|
|
13
|
-
send: (chatId: string,
|
|
14
|
+
send: (chatId: string, message: FeishuOutgoingMessage) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FeishuOutgoingMessage = { kind: 'interactive'; title: string; text: string };
|
|
18
|
+
|
|
19
|
+
type FeishuCreateData = {
|
|
20
|
+
receive_id: string;
|
|
21
|
+
msg_type: 'interactive';
|
|
22
|
+
content: string;
|
|
14
23
|
};
|
|
15
24
|
|
|
16
25
|
export type NormalizedMessage = {
|
|
@@ -69,6 +78,26 @@ function resolveDomain(value: unknown): number {
|
|
|
69
78
|
return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
export function cleanFeishuText(text: string): string {
|
|
82
|
+
return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function cardMessage(text: string): FeishuOutgoingMessage {
|
|
86
|
+
return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toFeishuCreateData(chatId: string, message: FeishuOutgoingMessage): FeishuCreateData {
|
|
90
|
+
return {
|
|
91
|
+
receive_id: chatId,
|
|
92
|
+
msg_type: 'interactive',
|
|
93
|
+
content: JSON.stringify({
|
|
94
|
+
config: { wide_screen_mode: true },
|
|
95
|
+
header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
|
|
96
|
+
elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
|
|
97
|
+
})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// Build the real SDK-backed transport from adapter config. appId/appSecret come
|
|
73
102
|
// from server config (appSecret only via server.local.json / env). The WSClient
|
|
74
103
|
// long connection delivers events; the Client REST call sends replies.
|
|
@@ -118,10 +147,10 @@ export function createFeishuTransport(config: Record<string, unknown>): FeishuTr
|
|
|
118
147
|
// Ignore: the daemon is shutting down regardless.
|
|
119
148
|
}
|
|
120
149
|
},
|
|
121
|
-
async send(chatId,
|
|
150
|
+
async send(chatId, message) {
|
|
122
151
|
await client.im.message.create({
|
|
123
152
|
params: { receive_id_type: 'chat_id' },
|
|
124
|
-
data:
|
|
153
|
+
data: toFeishuCreateData(chatId, message)
|
|
125
154
|
});
|
|
126
155
|
}
|
|
127
156
|
};
|
package/lib/server/protocol.ts
CHANGED
|
@@ -23,10 +23,26 @@ function taskSubcommand(subcommand: string): string {
|
|
|
23
23
|
export function commandHelp(): string {
|
|
24
24
|
return [
|
|
25
25
|
`agent-infra ${VERSION}`,
|
|
26
|
-
'Built-ins:
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
26
|
+
'Built-ins:',
|
|
27
|
+
'/help',
|
|
28
|
+
'/ping',
|
|
29
|
+
'/version',
|
|
30
|
+
'Read:',
|
|
31
|
+
'/sandbox ls',
|
|
32
|
+
'/sandbox show {ref}',
|
|
33
|
+
'/sandbox vm status',
|
|
34
|
+
'/task decisions {ref}',
|
|
35
|
+
'/task log {ref}',
|
|
36
|
+
'/task ls',
|
|
37
|
+
'/task show {ref}',
|
|
38
|
+
'/task status {ref}',
|
|
39
|
+
'Write:',
|
|
40
|
+
'/sandbox create {ref}',
|
|
41
|
+
'/sandbox start {ref}',
|
|
42
|
+
'Exec:',
|
|
43
|
+
'/decide {task-ref} {HD-id} {decision}',
|
|
44
|
+
'/run create-task {description}',
|
|
45
|
+
'/run {skill} {task-ref}'
|
|
30
46
|
].join('\n');
|
|
31
47
|
}
|
|
32
48
|
|
package/lib/task/commands/log.ts
CHANGED
|
@@ -13,7 +13,7 @@ completion time (or '(in progress)' while still running).
|
|
|
13
13
|
Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
|
|
14
14
|
A human-executed step shows AGENT as 'human' and, when it has no start marker,
|
|
15
15
|
a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
|
|
16
|
-
the verdict list, right after blockers/major/minor: manual-
|
|
16
|
+
the verdict list, right after blockers/major/minor: manual-validation
|
|
17
17
|
and human-decision (current ledger stage total).
|
|
18
18
|
`;
|
|
19
19
|
|
|
@@ -46,10 +46,6 @@ const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
|
|
|
46
46
|
// note these differ from the `.airc.json` long names claude-code/gemini-cli).
|
|
47
47
|
// Any other executor token (a human name, possibly CJK) is treated as human.
|
|
48
48
|
const KNOWN_AI_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode', 'cursor']);
|
|
49
|
-
const ENV_BLOCKED_RE = /\(\+\s*(\d+)\s+env-blocked\)/i;
|
|
50
|
-
// Same match plus any leading whitespace, so folding the count into the verdict
|
|
51
|
-
// text drops the redundant `(+ n env-blocked)` fragment without leaving a gap.
|
|
52
|
-
const ENV_BLOCKED_STRIP_RE = /\s*\(\+\s*\d+\s+env-blocked\)/i;
|
|
53
49
|
const REVIEW_STAGE_PREFIXES: { prefix: string; stage: ReviewStage }[] = [
|
|
54
50
|
{ prefix: 'Review Analysis', stage: 'analysis' },
|
|
55
51
|
{ prefix: 'Review Plan', stage: 'plan' },
|
|
@@ -126,9 +122,31 @@ function reviewStageForStep(step: string): ReviewStage | undefined {
|
|
|
126
122
|
return REVIEW_STAGE_PREFIXES.find(({ prefix }) => step.startsWith(prefix))?.stage;
|
|
127
123
|
}
|
|
128
124
|
|
|
125
|
+
function splitArtifactSuffix(note: string): { verdict: string; suffix: string } {
|
|
126
|
+
const arrow = note.indexOf(' → ');
|
|
127
|
+
return arrow === -1 ? { verdict: note, suffix: '' } : { verdict: note.slice(0, arrow), suffix: note.slice(arrow) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fieldNumber(field: string, label: string): number | undefined {
|
|
131
|
+
const trimmed = field.trim();
|
|
132
|
+
const colon = trimmed.indexOf(':');
|
|
133
|
+
if (colon === -1) return undefined;
|
|
134
|
+
if (trimmed.slice(0, colon).trim().toLowerCase() !== label) return undefined;
|
|
135
|
+
const value = Number(trimmed.slice(colon + 1).trim());
|
|
136
|
+
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
function humanValidationCount(note: string): number {
|
|
130
|
-
const
|
|
131
|
-
|
|
140
|
+
const { verdict } = splitArtifactSuffix(note);
|
|
141
|
+
for (const field of verdict.split(',')) {
|
|
142
|
+
const value = fieldNumber(field, 'manual-validation');
|
|
143
|
+
if (value !== undefined) return value;
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isHumanCountField(field: string): boolean {
|
|
149
|
+
return fieldNumber(field, 'manual-validation') !== undefined || fieldNumber(field, 'human-decision') !== undefined;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
// A step is human-executed when its agent token is not a known AI token. Take
|
|
@@ -141,13 +159,17 @@ function isHumanAgent(agent: string): boolean {
|
|
|
141
159
|
|
|
142
160
|
// Fold the two human counts into a review row's verdict NOTE: comma-joined, right
|
|
143
161
|
// after the blockers/major/minor list and before the ` → artifact` link, mirroring
|
|
144
|
-
// the review count line.
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
162
|
+
// the review count line. Review done notes already carry `Manual-validation` as a
|
|
163
|
+
// source field, so build the final verdict field list once instead of cleaning a
|
|
164
|
+
// previously rendered string.
|
|
165
|
+
function foldHumanCounts(note: string, decisions: number, manualValidation: number): string {
|
|
166
|
+
const { verdict, suffix } = splitArtifactSuffix(note);
|
|
167
|
+
const fields = verdict
|
|
168
|
+
.split(',')
|
|
169
|
+
.map((field) => field.trim())
|
|
170
|
+
.filter((field) => field !== '' && !isHumanCountField(field));
|
|
171
|
+
const group = `Manual-validation: ${manualValidation}, Human-decision: ${decisions}`;
|
|
172
|
+
return `${[...fields, group].join(', ')}${suffix}`;
|
|
151
173
|
}
|
|
152
174
|
|
|
153
175
|
function log(args: string[] = []): void {
|