agentxchain 2.155.73 → 2.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/agentxchain.js +22 -0
  2. package/dashboard/app.js +54 -0
  3. package/dashboard/components/org-audit-trail.js +161 -0
  4. package/dashboard/components/org-history.js +140 -0
  5. package/dashboard/components/org-overview.js +145 -0
  6. package/dashboard/components/org-runs.js +168 -0
  7. package/dashboard/index.html +4 -0
  8. package/package.json +2 -1
  9. package/src/commands/ci-report.js +80 -0
  10. package/src/commands/doctor.js +22 -1
  11. package/src/commands/intake-approve.js +1 -0
  12. package/src/commands/replay.js +1 -0
  13. package/src/commands/run.js +47 -0
  14. package/src/commands/serve.js +64 -0
  15. package/src/commands/step.js +16 -0
  16. package/src/commands/verify.js +1 -0
  17. package/src/lib/adapters/local-cli-adapter.js +147 -0
  18. package/src/lib/api/execution-worker.js +192 -0
  19. package/src/lib/api/hosted-runner.js +494 -0
  20. package/src/lib/api/job-queue.js +152 -0
  21. package/src/lib/api/org-state-aggregator.js +428 -0
  22. package/src/lib/api/project-registry.js +148 -0
  23. package/src/lib/api/protocol-bridge.js +476 -0
  24. package/src/lib/approval-policy.js +12 -0
  25. package/src/lib/ci-reporter.js +188 -0
  26. package/src/lib/claude-local-auth.js +75 -1
  27. package/src/lib/connector-probe.js +21 -0
  28. package/src/lib/continuous-run.js +9 -0
  29. package/src/lib/dashboard/bridge-server.js +10 -5
  30. package/src/lib/governed-state.js +1 -1
  31. package/src/lib/intake.js +32 -6
  32. package/src/lib/normalized-config.js +2 -0
  33. package/src/lib/scope-overlap.js +214 -0
  34. package/src/lib/turn-checkpoint.js +21 -0
  35. package/src/lib/turn-result-validator.js +5 -0
  36. package/src/lib/validation.js +11 -3
  37. package/src/lib/verification-replay.js +125 -4
  38. package/src/lib/vision-reader.js +7 -2
@@ -8,7 +8,7 @@ import { isOperationalPath } from './repo-observer.js';
8
8
 
9
9
  export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
10
10
 
11
- export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS }) {
11
+ export function replayVerificationMachineEvidence({ root, verification, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS, allowCommandExecution = false }) {
12
12
  const verifiedAt = new Date().toISOString();
13
13
  const machineEvidence = Array.isArray(verification?.machine_evidence)
14
14
  ? verification.machine_evidence
@@ -28,6 +28,27 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
28
28
  return payload;
29
29
  }
30
30
 
31
+ // SECURITY (verification-replay hardening): agent-declared machine_evidence
32
+ // commands are UNTRUSTED. Fail-safe default — they are recorded but NOT executed
33
+ // unless the operator has explicitly opted in (allowCommandExecution). When
34
+ // executed, they never run through a shell (see replayEvidenceCommand).
35
+ if (!allowCommandExecution) {
36
+ payload.overall = 'not_executed';
37
+ payload.reason = 'Command replay disabled: agent-declared verification commands are not executed unless explicitly enabled by the operator.';
38
+ payload.commands = machineEvidence.map((entry, index) => ({
39
+ index,
40
+ command: entry?.command ?? null,
41
+ declared_exit_code: entry?.exit_code,
42
+ actual_exit_code: null,
43
+ matched: false,
44
+ executed: false,
45
+ timed_out: false,
46
+ signal: null,
47
+ error: null,
48
+ }));
49
+ return payload;
50
+ }
51
+
31
52
  const workspaceGuard = createReplayWorkspaceGuard(root);
32
53
  try {
33
54
  payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
@@ -41,11 +62,33 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
41
62
  return payload;
42
63
  }
43
64
 
65
+ // SECURITY: agent-declared commands are NEVER run through a shell. The command is
66
+ // resolved to an argv vector and executed with shell:false, so shell metacharacters
67
+ // (; | && $() backticks globs) cannot chain, pipe, expand, or substitute. (A single
68
+ // declared binary still runs on the host — that is gated by the explicit
69
+ // allowCommandExecution opt-in upstream; a future sandbox/allowlist can constrain
70
+ // which binaries are permitted.)
44
71
  export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS) {
45
- const result = spawnSync(entry.command, {
72
+ const argv = resolveEvidenceArgv(entry);
73
+ if (argv.length === 0) {
74
+ return {
75
+ index,
76
+ command: entry?.command ?? null,
77
+ declared_exit_code: entry?.exit_code,
78
+ actual_exit_code: null,
79
+ matched: false,
80
+ executed: false,
81
+ timed_out: false,
82
+ signal: null,
83
+ error: 'No executable command or argv vector could be resolved for this evidence entry.',
84
+ };
85
+ }
86
+
87
+ const [file, ...args] = argv;
88
+ const result = spawnSync(file, args, {
46
89
  cwd: root,
47
90
  encoding: 'utf8',
48
- shell: true,
91
+ shell: false,
49
92
  timeout: timeoutMs,
50
93
  maxBuffer: 1024 * 1024,
51
94
  });
@@ -56,16 +99,94 @@ export function replayEvidenceCommand(root, entry, index, timeoutMs = DEFAULT_VE
56
99
 
57
100
  return {
58
101
  index,
59
- command: entry.command,
102
+ command: entry.command ?? argv.join(' '),
60
103
  declared_exit_code: entry.exit_code,
61
104
  actual_exit_code: actualExitCode,
62
105
  matched: actualExitCode === entry.exit_code,
106
+ executed: true,
63
107
  timed_out: timedOut,
64
108
  signal: result.signal || null,
65
109
  error: errorMessage,
66
110
  };
67
111
  }
68
112
 
113
+ // Resolve an evidence entry to an argv vector for shell-free execution.
114
+ // Prefers a structured `argv` string array; falls back to safe-tokenizing `command`.
115
+ export function resolveEvidenceArgv(entry) {
116
+ if (Array.isArray(entry?.argv) && entry.argv.length > 0 && entry.argv.every((a) => typeof a === 'string')) {
117
+ return entry.argv.slice();
118
+ }
119
+ if (typeof entry?.command === 'string' && entry.command.trim()) {
120
+ return tokenizeCommand(entry.command.trim());
121
+ }
122
+ return [];
123
+ }
124
+
125
+ // Minimal SAFE tokenizer: splits on whitespace, honoring single and double quotes
126
+ // and backslash escapes (so e.g. `node -e "{\"ok\":true}"` keeps its escaped quotes).
127
+ // Performs NO shell expansion — variables, globs, pipes, chaining, and command
128
+ // substitution are preserved as literal characters, never interpreted.
129
+ export function tokenizeCommand(command) {
130
+ if (typeof command !== 'string') {
131
+ return [];
132
+ }
133
+ const tokens = [];
134
+ let current = '';
135
+ let hasToken = false;
136
+ let i = 0;
137
+ const n = command.length;
138
+ while (i < n) {
139
+ const ch = command[i];
140
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
141
+ if (hasToken) {
142
+ tokens.push(current);
143
+ current = '';
144
+ hasToken = false;
145
+ }
146
+ i += 1;
147
+ continue;
148
+ }
149
+ hasToken = true;
150
+ if (ch === "'") {
151
+ // Single-quoted: literal run until the next single quote (no escapes, POSIX-style).
152
+ i += 1;
153
+ while (i < n && command[i] !== "'") {
154
+ current += command[i];
155
+ i += 1;
156
+ }
157
+ i += 1; // consume closing quote (or run off the end)
158
+ continue;
159
+ }
160
+ if (ch === '"') {
161
+ // Double-quoted: backslash escapes only the structural `"` and `\` characters.
162
+ i += 1;
163
+ while (i < n && command[i] !== '"') {
164
+ if (command[i] === '\\' && i + 1 < n && (command[i + 1] === '"' || command[i + 1] === '\\')) {
165
+ current += command[i + 1];
166
+ i += 2;
167
+ } else {
168
+ current += command[i];
169
+ i += 1;
170
+ }
171
+ }
172
+ i += 1; // consume closing quote (or run off the end)
173
+ continue;
174
+ }
175
+ if (ch === '\\' && i + 1 < n) {
176
+ // Bare backslash escape: take the next character literally.
177
+ current += command[i + 1];
178
+ i += 2;
179
+ continue;
180
+ }
181
+ current += ch;
182
+ i += 1;
183
+ }
184
+ if (hasToken) {
185
+ tokens.push(current);
186
+ }
187
+ return tokens;
188
+ }
189
+
69
190
  export function summarizeVerificationReplay(payload) {
70
191
  if (!payload) {
71
192
  return null;
@@ -16,6 +16,11 @@ import { join, resolve as pathResolve, isAbsolute } from 'node:path';
16
16
  import { createHash } from 'node:crypto';
17
17
 
18
18
  const ROADMAP_TRACKING_ANNOTATION_PATTERN = /<!--\s*tracking\s*:[\s\S]*?-->/i;
19
+ // RB-3: a milestone left unchecked but annotated as delivered/done/shipped is
20
+ // already-built work — skip it so the loop derives genuinely undelivered work
21
+ // instead of re-picking finished items. Distinct from generic comments like
22
+ // `<!-- owner: dev -->`, which stay actionable.
23
+ const ROADMAP_DELIVERED_ANNOTATION_PATTERN = /<!--[^>]*\b(delivered|shipped|implemented|completed|done)\b[^>]*-->/i;
19
24
 
20
25
  export function stripRoadmapTrackingAnnotations(text) {
21
26
  if (typeof text !== 'string') return '';
@@ -266,7 +271,7 @@ export function deriveRoadmapCandidates(root, roadmapPath = '.planning/ROADMAP.m
266
271
 
267
272
  const uncheckedMatch = line.match(/^\s*[-*]\s+\[\s\]\s+(.+?)\s*$/);
268
273
  if (!uncheckedMatch || !currentMilestone) continue;
269
- if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line)) continue;
274
+ if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line) || ROADMAP_DELIVERED_ANNOTATION_PATTERN.test(line)) continue;
270
275
 
271
276
  const goal = stripRoadmapTrackingAnnotations(uncheckedMatch[1]);
272
277
  if (!goal) continue;
@@ -493,7 +498,7 @@ export function detectRoadmapExhaustedVisionOpen(root, visionPath, roadmapPath =
493
498
  continue;
494
499
  }
495
500
  if (currentMilestone && /^\s*[-*]\s+\[\s\]/.test(line)) {
496
- if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line)) continue;
501
+ if (ROADMAP_TRACKING_ANNOTATION_PATTERN.test(line) || ROADMAP_DELIVERED_ANNOTATION_PATTERN.test(line)) continue;
497
502
  hasUnchecked = true;
498
503
  }
499
504
  }