@fitlab-ai/agent-infra 0.9.3 → 0.9.4
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 +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -123
- package/dist/lib/agent-clients/adapter.js +92 -2
- 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/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/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- 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 +122 -18
- 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 +109 -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 +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -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 +103 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -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 +363 -28
- package/dist/lib/task/resolve-ref.js +41 -1
- package/lib/agent-clients/adapter.ts +126 -2
- 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/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/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +81 -11
- 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 +126 -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 +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -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 +143 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -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 +501 -33
- package/lib/task/resolve-ref.ts +46 -1
- package/package.json +1 -1
- 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 +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -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 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -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/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/.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
|
@@ -64,69 +64,9 @@ RUN printf '%s\n' \
|
|
|
64
64
|
'set -g mouse on' \
|
|
65
65
|
'set -g status-interval 1' \
|
|
66
66
|
'set -g status-right-length 80' \
|
|
67
|
-
"set -g status-right '
|
|
67
|
+
"set -g status-right '%H:%M'" \
|
|
68
68
|
> /etc/tmux.conf
|
|
69
69
|
|
|
70
|
-
RUN cat > /usr/local/bin/cc-token-status <<'SCRIPT' && chmod +x /usr/local/bin/cc-token-status
|
|
71
|
-
#!/bin/sh
|
|
72
|
-
set -eu
|
|
73
|
-
|
|
74
|
-
SETTINGS_FILE="/home/devuser/.claude/settings.json"
|
|
75
|
-
if [ -r "$SETTINGS_FILE" ] && jq -e '
|
|
76
|
-
def has_nonempty($v):
|
|
77
|
-
if ($v | type) == "string" then ($v | gsub("^\\s+|\\s+$"; "") | length) > 0 else false end;
|
|
78
|
-
(if type == "object" then . else {} end) as $settings
|
|
79
|
-
| (if ($settings.env | type) == "object" then $settings.env else {} end) as $env
|
|
80
|
-
| has_nonempty($env.ANTHROPIC_AUTH_TOKEN)
|
|
81
|
-
or has_nonempty($env.ANTHROPIC_API_KEY)
|
|
82
|
-
or has_nonempty($settings.apiKeyHelper)
|
|
83
|
-
' "$SETTINGS_FILE" >/dev/null 2>&1; then
|
|
84
|
-
exit 0
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
CRED_FILE="/home/devuser/.claude/.credentials.json"
|
|
88
|
-
[ -r "$CRED_FILE" ] || exit 0
|
|
89
|
-
|
|
90
|
-
EXPIRES_MS=$(jq -r '(.claudeAiOauth.expiresAt // .expiresAt) // empty' "$CRED_FILE" 2>/dev/null || true)
|
|
91
|
-
case "$EXPIRES_MS" in
|
|
92
|
-
''|*[!0-9]*) exit 0 ;;
|
|
93
|
-
esac
|
|
94
|
-
|
|
95
|
-
NOW_MS=$(($(date +%s) * 1000))
|
|
96
|
-
DIFF_MS=$((EXPIRES_MS - NOW_MS))
|
|
97
|
-
DIFF_S=$((DIFF_MS / 1000))
|
|
98
|
-
|
|
99
|
-
DIM='#[fg=colour245]'
|
|
100
|
-
YELLOW='#[fg=yellow]'
|
|
101
|
-
YELLOW_BOLD='#[fg=yellow,bold]'
|
|
102
|
-
RED_BOLD='#[fg=red,bold]'
|
|
103
|
-
RED_REV='#[fg=red,reverse]'
|
|
104
|
-
RESET='#[default]'
|
|
105
|
-
|
|
106
|
-
if [ "$DIFF_S" -le 0 ]; then
|
|
107
|
-
ELAPSED=$(( -DIFF_S ))
|
|
108
|
-
M=$((ELAPSED / 60))
|
|
109
|
-
printf '%sClaude Code auth EXPIRED %dm ago%s' "$RED_REV" "$M" "$RESET"
|
|
110
|
-
elif [ "$DIFF_S" -lt 60 ]; then
|
|
111
|
-
printf '%sClaude Code auth expires in %ds%s' "$RED_BOLD" "$DIFF_S" "$RESET"
|
|
112
|
-
elif [ "$DIFF_S" -lt 300 ]; then
|
|
113
|
-
M=$((DIFF_S / 60))
|
|
114
|
-
S=$((DIFF_S % 60))
|
|
115
|
-
printf '%sClaude Code auth expires in %dm %ds%s' "$RED_BOLD" "$M" "$S" "$RESET"
|
|
116
|
-
elif [ "$DIFF_S" -lt 1800 ]; then
|
|
117
|
-
M=$((DIFF_S / 60))
|
|
118
|
-
printf '%sClaude Code auth expires in %dm%s' "$YELLOW_BOLD" "$M" "$RESET"
|
|
119
|
-
elif [ "$DIFF_S" -lt 3600 ]; then
|
|
120
|
-
M=$((DIFF_S / 60))
|
|
121
|
-
printf '%sClaude Code auth expires in %dm%s' "$YELLOW" "$M" "$RESET"
|
|
122
|
-
else
|
|
123
|
-
TOTAL_M=$((DIFF_S / 60))
|
|
124
|
-
H=$((TOTAL_M / 60))
|
|
125
|
-
M=$((TOTAL_M % 60))
|
|
126
|
-
printf '%sClaude Code auth expires in %dh %dm%s' "$DIM" "$H" "$M" "$RESET"
|
|
127
|
-
fi
|
|
128
|
-
SCRIPT
|
|
129
|
-
|
|
130
70
|
RUN cat > /usr/local/bin/sandbox-dotfiles-link <<'SCRIPT' && chmod +x /usr/local/bin/sandbox-dotfiles-link
|
|
131
71
|
#!/bin/sh
|
|
132
72
|
# Mirror /dotfiles/ tree as symlinks under $HOME/, overwriting any image-baked
|
|
@@ -136,23 +76,34 @@ set -eu
|
|
|
136
76
|
DOTFILES_SRC=/dotfiles
|
|
137
77
|
[ -d "$DOTFILES_SRC" ] || exit 0
|
|
138
78
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
target="$HOME/$rel"
|
|
143
|
-
case "$rel" in
|
|
79
|
+
is_dotfiles_excluded() {
|
|
80
|
+
candidate=$1
|
|
81
|
+
case "$candidate" in
|
|
144
82
|
.ssh|.ssh/*|\
|
|
145
83
|
.gnupg|.gnupg/*|\
|
|
146
|
-
.claude|.claude/*|\
|
|
147
|
-
.codex|.codex/*|\
|
|
148
|
-
.gemini|.gemini/*|\
|
|
149
|
-
.config/opencode|.config/opencode/*|\
|
|
150
|
-
.local/share/opencode|.local/share/opencode/*|\
|
|
151
84
|
.host-shell-config|.host-shell-config/*|\
|
|
152
85
|
.gitconfig|.gitignore_global|.stCommitMsg|.bash_aliases|README.md)
|
|
153
|
-
|
|
86
|
+
return 0 ;;
|
|
154
87
|
esac
|
|
155
88
|
|
|
89
|
+
[ -r /etc/agent-infra/dotfiles-exclusions ] || return 1
|
|
90
|
+
while IFS= read -r prefix; do
|
|
91
|
+
[ -n "$prefix" ] || continue
|
|
92
|
+
case "$candidate" in
|
|
93
|
+
"$prefix"|"$prefix"/*) return 0 ;;
|
|
94
|
+
esac
|
|
95
|
+
done < /etc/agent-infra/dotfiles-exclusions
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cd "$DOTFILES_SRC"
|
|
100
|
+
find . -type f -print | while IFS= read -r rel; do
|
|
101
|
+
rel=${rel#./}
|
|
102
|
+
target="$HOME/$rel"
|
|
103
|
+
if is_dotfiles_excluded "$rel"; then
|
|
104
|
+
continue
|
|
105
|
+
fi
|
|
106
|
+
|
|
156
107
|
mkdir -p "$(dirname "$target")"
|
|
157
108
|
if [ -d "$target" ] && [ ! -L "$target" ]; then
|
|
158
109
|
printf 'sandbox-dotfiles-link: skipping %s (existing directory; use nested path like %s/<file> instead)\n' "$target" "$rel" >&2
|
|
@@ -198,8 +149,7 @@ fi
|
|
|
198
149
|
exec tmux new-session -s "$SESSION"
|
|
199
150
|
SCRIPT
|
|
200
151
|
|
|
201
|
-
RUN test -s /usr/local/bin/
|
|
202
|
-
test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
|
|
152
|
+
RUN test -s /usr/local/bin/sandbox-dotfiles-link && test -x /usr/local/bin/sandbox-dotfiles-link && \
|
|
203
153
|
test -s /usr/local/bin/sandbox-tmux-entry && test -x /usr/local/bin/sandbox-tmux-entry
|
|
204
154
|
|
|
205
155
|
ENV LANG=en_US.UTF-8
|
|
@@ -64,15 +64,23 @@ type SandboxResolvedToolState = Readonly<{
|
|
|
64
64
|
|
|
65
65
|
type SandboxHookCreateContext = Readonly<{
|
|
66
66
|
hostHome: string;
|
|
67
|
+
hostEnv?: Readonly<NodeJS.ProcessEnv>;
|
|
68
|
+
project: string;
|
|
67
69
|
resolvedTools: readonly SandboxResolvedToolState[];
|
|
68
70
|
}>;
|
|
69
71
|
|
|
72
|
+
type SandboxHookEnterContext = Readonly<{
|
|
73
|
+
hostHome: string;
|
|
74
|
+
hostEnv?: Readonly<NodeJS.ProcessEnv>;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
70
77
|
type AgentClientSandboxHookContext = Readonly<{
|
|
71
78
|
signal: AbortSignal;
|
|
72
79
|
runCommand(command: SandboxHookCommand): Promise<SandboxHookCommandResult>;
|
|
73
80
|
config?: Readonly<Record<string, unknown>>;
|
|
74
81
|
plan?: Readonly<Record<string, unknown>>;
|
|
75
82
|
create?: SandboxHookCreateContext;
|
|
83
|
+
enter?: SandboxHookEnterContext;
|
|
76
84
|
}>;
|
|
77
85
|
|
|
78
86
|
type AgentClientSandboxHook = Readonly<{
|
|
@@ -113,11 +121,17 @@ type AgentClientSandboxRecoveryCheck = Readonly<{
|
|
|
113
121
|
repair?: SandboxRecoveryRepair;
|
|
114
122
|
}>;
|
|
115
123
|
|
|
124
|
+
type AgentClientSandboxImageDescriptor = Readonly<{
|
|
125
|
+
dockerfileFragment?: string;
|
|
126
|
+
dotfilesExclusions?: readonly string[];
|
|
127
|
+
}>;
|
|
128
|
+
|
|
116
129
|
type AgentClientSandboxDescriptor = Readonly<{
|
|
117
130
|
createTool(context: SandboxToolContext): SandboxTool;
|
|
118
131
|
aliases: readonly SandboxAlias[];
|
|
119
132
|
hooks: readonly AgentClientSandboxHook[];
|
|
120
133
|
recoveryChecks?: readonly AgentClientSandboxRecoveryCheck[];
|
|
134
|
+
image?: AgentClientSandboxImageDescriptor;
|
|
121
135
|
}>;
|
|
122
136
|
|
|
123
137
|
export { SANDBOX_HOOK_PHASES };
|
|
@@ -126,6 +140,7 @@ export type {
|
|
|
126
140
|
AgentClientSandboxHook,
|
|
127
141
|
AgentClientSandboxHookContext,
|
|
128
142
|
AgentClientSandboxHookResult,
|
|
143
|
+
AgentClientSandboxImageDescriptor,
|
|
129
144
|
AgentClientSandboxRecoveryCheck,
|
|
130
145
|
SandboxAlias,
|
|
131
146
|
SandboxHookCommand,
|
|
@@ -133,6 +148,7 @@ export type {
|
|
|
133
148
|
SandboxHookPhase,
|
|
134
149
|
SandboxHookStatus,
|
|
135
150
|
SandboxHookCreateContext,
|
|
151
|
+
SandboxHookEnterContext,
|
|
136
152
|
SandboxRecoveryFindingDescriptor,
|
|
137
153
|
SandboxRecoveryProbe,
|
|
138
154
|
SandboxRecoveryRepair,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isRemovedHashShortIdInput } from '../task/short-id.ts';
|
|
4
|
+
|
|
5
|
+
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
|
+
const SHORT_ID_RE = /^\d+$/;
|
|
7
|
+
|
|
8
|
+
export type SandboxWorkspaceIdentity =
|
|
9
|
+
| Readonly<{ mode: 'task-bound'; taskId: string; shortId: string }>
|
|
10
|
+
| Readonly<{ mode: 'branch-only' }>;
|
|
11
|
+
|
|
12
|
+
export type SandboxContainerWorkspaceIdentity =
|
|
13
|
+
| Readonly<{ mode: 'task-bound'; taskId: string }>
|
|
14
|
+
| Readonly<{ mode: 'branch-only' }>
|
|
15
|
+
| Readonly<{ mode: 'legacy-invalid' }>;
|
|
16
|
+
|
|
17
|
+
export type SandboxTarget = Readonly<{
|
|
18
|
+
requestedRef: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
workspace: SandboxWorkspaceIdentity;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
function stripQuotes(value: string): string {
|
|
24
|
+
return value.replace(/^(?:"([^"]*)"|'([^']*)')$/, '$1$2');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function taskBranch(taskMd: string, taskId: string): string {
|
|
28
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
29
|
+
const value = content.match(/^branch:\s*(.+)$/m)?.[1]?.trim()
|
|
30
|
+
?? content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m)?.[1]?.trim();
|
|
31
|
+
if (!value) throw new Error(`Task ${taskId} has no branch field in task.md`);
|
|
32
|
+
return stripQuotes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function activeRegistry(repoRoot: string): Record<string, string> {
|
|
36
|
+
const registryPath = path.join(repoRoot, '.agents', 'workspace', 'active', '.short-ids.json');
|
|
37
|
+
if (!fs.existsSync(registryPath)) return {};
|
|
38
|
+
const value = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as unknown;
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
40
|
+
throw new Error('Active task short-id registry is invalid');
|
|
41
|
+
}
|
|
42
|
+
const ids = 'ids' in value && value.ids && typeof value.ids === 'object' && !Array.isArray(value.ids)
|
|
43
|
+
? value.ids as Record<string, unknown>
|
|
44
|
+
: value as Record<string, unknown>;
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(ids).filter((entry): entry is [string, string] =>
|
|
47
|
+
SHORT_ID_RE.test(entry[0]) && typeof entry[1] === 'string' && TASK_ID_RE.test(entry[1])
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function taskIdentity(repoRoot: string, taskId: string, registry: Record<string, string>): SandboxWorkspaceIdentity {
|
|
53
|
+
const taskMd = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
|
|
54
|
+
if (!fs.existsSync(taskMd)) throw new Error(`Active task not found: ${taskId}`);
|
|
55
|
+
const shortIds = Object.entries(registry).filter(([, id]) => id === taskId).map(([shortId]) => shortId);
|
|
56
|
+
if (shortIds.length !== 1) {
|
|
57
|
+
throw new Error(`Task ${taskId} must have exactly one active short id`);
|
|
58
|
+
}
|
|
59
|
+
return { mode: 'task-bound', taskId, shortId: shortIds[0]! };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
|
|
63
|
+
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
64
|
+
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
65
|
+
}
|
|
66
|
+
const registry = activeRegistry(repoRoot);
|
|
67
|
+
if (SHORT_ID_RE.test(requestedRef)) {
|
|
68
|
+
const matchingShortIds = Object.keys(registry).filter((shortId) =>
|
|
69
|
+
shortId === requestedRef || Number(shortId) === Number(requestedRef)
|
|
70
|
+
);
|
|
71
|
+
if (matchingShortIds.length > 1) {
|
|
72
|
+
throw new Error(`Short id '${requestedRef}' is ambiguous in active task registry`);
|
|
73
|
+
}
|
|
74
|
+
const canonicalShortId = matchingShortIds[0];
|
|
75
|
+
const taskId = canonicalShortId ? registry[canonicalShortId] : undefined;
|
|
76
|
+
if (!taskId) throw new Error(`Short id '${requestedRef}' not found in active task registry`);
|
|
77
|
+
const workspace = taskIdentity(repoRoot, taskId, registry);
|
|
78
|
+
return { requestedRef, branch: taskBranch(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), taskId), workspace };
|
|
79
|
+
}
|
|
80
|
+
if (TASK_ID_RE.test(requestedRef)) {
|
|
81
|
+
const workspace = taskIdentity(repoRoot, requestedRef, registry);
|
|
82
|
+
return { requestedRef, branch: taskBranch(path.join(repoRoot, '.agents', 'workspace', 'active', requestedRef, 'task.md'), requestedRef), workspace };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const activeRoot = path.join(repoRoot, '.agents', 'workspace', 'active');
|
|
86
|
+
const matches = fs.existsSync(activeRoot)
|
|
87
|
+
? fs.readdirSync(activeRoot, { withFileTypes: true }).flatMap((entry) => {
|
|
88
|
+
if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) return [];
|
|
89
|
+
const taskMd = path.join(activeRoot, entry.name, 'task.md');
|
|
90
|
+
if (!fs.existsSync(taskMd)) return [];
|
|
91
|
+
return taskBranch(taskMd, entry.name) === requestedRef ? [entry.name] : [];
|
|
92
|
+
})
|
|
93
|
+
: [];
|
|
94
|
+
if (matches.length > 1) {
|
|
95
|
+
throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple active tasks`);
|
|
96
|
+
}
|
|
97
|
+
if (matches.length === 1) {
|
|
98
|
+
const taskId = matches[0]!;
|
|
99
|
+
return { requestedRef, branch: requestedRef, workspace: taskIdentity(repoRoot, taskId, registry) };
|
|
100
|
+
}
|
|
101
|
+
return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseSandboxWorkspaceIdentity(
|
|
105
|
+
labels: Readonly<Record<string, string>>,
|
|
106
|
+
keys: Readonly<{ mode: string; taskId: string }>
|
|
107
|
+
): SandboxContainerWorkspaceIdentity {
|
|
108
|
+
const mode = labels[keys.mode];
|
|
109
|
+
const taskId = labels[keys.taskId];
|
|
110
|
+
if (mode === 'branch-only' && taskId === undefined) return { mode: 'branch-only' };
|
|
111
|
+
if (mode === 'task-bound' && typeof taskId === 'string' && TASK_ID_RE.test(taskId)) {
|
|
112
|
+
return { mode: 'task-bound', taskId };
|
|
113
|
+
}
|
|
114
|
+
return { mode: 'legacy-invalid' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function sameSandboxWorkspaceIdentity(
|
|
118
|
+
left: SandboxWorkspaceIdentity | SandboxContainerWorkspaceIdentity,
|
|
119
|
+
right: SandboxWorkspaceIdentity | SandboxContainerWorkspaceIdentity
|
|
120
|
+
): boolean {
|
|
121
|
+
return left.mode === right.mode
|
|
122
|
+
&& (left.mode !== 'task-bound' || (right.mode === 'task-bound' && left.taskId === right.taskId));
|
|
123
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { SandboxWorkspaceIdentity } from './workspace-identity.ts';
|
|
5
|
+
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
6
|
+
|
|
7
|
+
export type SandboxWorkspaceView = Readonly<{
|
|
8
|
+
root: string;
|
|
9
|
+
taskMountPath: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export function sandboxWorkspaceViewPaths(params: Readonly<{
|
|
13
|
+
base: string;
|
|
14
|
+
project: string;
|
|
15
|
+
container: string;
|
|
16
|
+
identity: SandboxWorkspaceIdentity;
|
|
17
|
+
}>): SandboxWorkspaceView {
|
|
18
|
+
const identityKey = params.identity.mode === 'task-bound'
|
|
19
|
+
? `task-bound:${params.identity.taskId}`
|
|
20
|
+
: 'branch-only';
|
|
21
|
+
const digest = createHash('sha256').update(identityKey).digest('hex').slice(0, 16);
|
|
22
|
+
const projectRoot = path.resolve(params.base, params.project);
|
|
23
|
+
const root = path.resolve(projectRoot, params.container, digest);
|
|
24
|
+
return {
|
|
25
|
+
root,
|
|
26
|
+
taskMountPath: params.identity.mode === 'task-bound'
|
|
27
|
+
? path.join(root, 'active', params.identity.taskId)
|
|
28
|
+
: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SandboxControlSetup = Readonly<{
|
|
33
|
+
channelDir: string;
|
|
34
|
+
manifestPath: string;
|
|
35
|
+
token: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
export function sandboxControlPaths(params: Readonly<{
|
|
39
|
+
base: string;
|
|
40
|
+
project: string;
|
|
41
|
+
container: string;
|
|
42
|
+
identity: SandboxWorkspaceIdentity;
|
|
43
|
+
}>): Readonly<{ root: string; channelDir: string; manifestPath: string }> {
|
|
44
|
+
const identityKey = params.identity.mode === 'task-bound'
|
|
45
|
+
? `task-bound:${params.identity.taskId}`
|
|
46
|
+
: 'branch-only';
|
|
47
|
+
const digest = createHash('sha256').update(identityKey).digest('hex').slice(0, 16);
|
|
48
|
+
const root = path.resolve(params.base, params.project, params.container, digest);
|
|
49
|
+
return { root, channelDir: path.join(root, 'channel'), manifestPath: path.join(root, 'manifest.json') };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
53
|
+
const relative = path.relative(expectedBase, directory);
|
|
54
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
55
|
+
throw new Error(`Sandbox workspace view escapes its configured root: ${directory}`);
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
|
|
58
|
+
throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
63
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
64
|
+
const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
|
|
65
|
+
const source = path.join(activeRoot, taskId);
|
|
66
|
+
const stat = fs.lstatSync(source);
|
|
67
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
68
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
69
|
+
}
|
|
70
|
+
const canonical = fs.realpathSync.native(source);
|
|
71
|
+
if (path.dirname(canonical) !== activeRoot) {
|
|
72
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the active workspace`);
|
|
73
|
+
}
|
|
74
|
+
return canonical;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
78
|
+
base: string;
|
|
79
|
+
project: string;
|
|
80
|
+
container: string;
|
|
81
|
+
identity: SandboxWorkspaceIdentity;
|
|
82
|
+
}>): SandboxWorkspaceView {
|
|
83
|
+
const projectRoot = path.resolve(params.base, params.project);
|
|
84
|
+
const { root, taskMountPath } = sandboxWorkspaceViewPaths(params);
|
|
85
|
+
assertSafeDirectory(projectRoot, path.resolve(params.base));
|
|
86
|
+
assertSafeDirectory(root, projectRoot);
|
|
87
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
88
|
+
for (const state of ['active', 'completed', 'blocked', 'archive']) {
|
|
89
|
+
const stateDir = path.join(root, state);
|
|
90
|
+
assertSafeDirectory(stateDir, root);
|
|
91
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
92
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const active = path.join(root, 'active');
|
|
96
|
+
const registry = {
|
|
97
|
+
version: 1,
|
|
98
|
+
ids: params.identity.mode === 'task-bound'
|
|
99
|
+
? { [params.identity.shortId]: params.identity.taskId }
|
|
100
|
+
: {}
|
|
101
|
+
};
|
|
102
|
+
fs.writeFileSync(path.join(active, '.short-ids.json'), `${JSON.stringify(registry)}\n`, {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
mode: 0o600
|
|
105
|
+
});
|
|
106
|
+
if (taskMountPath) {
|
|
107
|
+
assertSafeDirectory(taskMountPath, active);
|
|
108
|
+
fs.mkdirSync(taskMountPath, { recursive: true, mode: 0o700 });
|
|
109
|
+
}
|
|
110
|
+
return { root, taskMountPath };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function materializeSandboxControl(params: Readonly<{
|
|
114
|
+
base: string;
|
|
115
|
+
repoRoot: string;
|
|
116
|
+
project: string;
|
|
117
|
+
container: string;
|
|
118
|
+
branch: string;
|
|
119
|
+
identity: SandboxWorkspaceIdentity;
|
|
120
|
+
}>): SandboxControlSetup {
|
|
121
|
+
const { root, channelDir, manifestPath } = sandboxControlPaths(params);
|
|
122
|
+
assertSafeDirectory(root, path.resolve(params.base));
|
|
123
|
+
for (const queue of ['requests', 'responses']) {
|
|
124
|
+
const directory = path.join(channelDir, queue);
|
|
125
|
+
assertSafeDirectory(directory, root);
|
|
126
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
127
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
128
|
+
}
|
|
129
|
+
const token = randomBytes(32).toString('hex');
|
|
130
|
+
const manifest: SandboxControlManifest = {
|
|
131
|
+
version: 1,
|
|
132
|
+
repoRoot: fs.realpathSync.native(params.repoRoot),
|
|
133
|
+
project: params.project,
|
|
134
|
+
container: params.container,
|
|
135
|
+
branch: params.branch,
|
|
136
|
+
mode: params.identity.mode,
|
|
137
|
+
taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
|
|
138
|
+
token,
|
|
139
|
+
channelDir
|
|
140
|
+
};
|
|
141
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
142
|
+
return { channelDir, manifestPath, token };
|
|
143
|
+
}
|