@ashlr/hub 2.2.0 → 3.0.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.
- package/CHANGELOG.md +55 -0
- package/README.md +59 -2
- package/dist/api/plugin.d.ts +1 -0
- package/dist/cli/backlog.js +2 -0
- package/dist/cli/backlog.js.map +1 -1
- package/dist/cli/digest.d.ts +6 -0
- package/dist/cli/digest.js +127 -2
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/eval-fixtures.d.ts +18 -0
- package/dist/cli/eval-fixtures.js +38 -0
- package/dist/cli/eval-fixtures.js.map +1 -0
- package/dist/cli/eval.d.ts +56 -0
- package/dist/cli/eval.js +283 -0
- package/dist/cli/eval.js.map +1 -0
- package/dist/cli/fleet.d.ts +27 -0
- package/dist/cli/fleet.js +222 -0
- package/dist/cli/fleet.js.map +1 -0
- package/dist/cli/goal.d.ts +15 -0
- package/dist/cli/goal.js +91 -0
- package/dist/cli/goal.js.map +1 -0
- package/dist/cli/help.js +10 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inbox.d.ts +6 -0
- package/dist/cli/inbox.js +183 -8
- package/dist/cli/inbox.js.map +1 -1
- package/dist/cli/index.js +42 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/loop.d.ts +16 -0
- package/dist/cli/loop.js +66 -0
- package/dist/cli/loop.js.map +1 -0
- package/dist/cli/mcp.d.ts +12 -0
- package/dist/cli/mcp.js +148 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/onboard.d.ts +16 -0
- package/dist/cli/onboard.js +71 -0
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/plugins.js +9 -8
- package/dist/cli/plugins.js.map +1 -1
- package/dist/cli/pulse.d.ts +10 -0
- package/dist/cli/pulse.js +261 -0
- package/dist/cli/pulse.js.map +1 -1
- package/dist/cli/run.js +21 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stack.d.ts +34 -0
- package/dist/cli/stack.js +221 -0
- package/dist/cli/stack.js.map +1 -0
- package/dist/cli/update.js +12 -5
- package/dist/cli/update.js.map +1 -1
- package/dist/core/daemon/loop.d.ts +10 -5
- package/dist/core/daemon/loop.js +167 -37
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/env-bridge.js +3 -2
- package/dist/core/env-bridge.js.map +1 -1
- package/dist/core/fleet/automerge-pass.d.ts +33 -0
- package/dist/core/fleet/automerge-pass.js +61 -0
- package/dist/core/fleet/automerge-pass.js.map +1 -0
- package/dist/core/fleet/quota.d.ts +70 -0
- package/dist/core/fleet/quota.js +198 -0
- package/dist/core/fleet/quota.js.map +1 -0
- package/dist/core/fleet/router.d.ts +46 -0
- package/dist/core/fleet/router.js +121 -0
- package/dist/core/fleet/router.js.map +1 -0
- package/dist/core/fleet/self.d.ts +58 -0
- package/dist/core/fleet/self.js +187 -0
- package/dist/core/fleet/self.js.map +1 -0
- package/dist/core/fleet/status.d.ts +54 -0
- package/dist/core/fleet/status.js +157 -0
- package/dist/core/fleet/status.js.map +1 -0
- package/dist/core/foundry/provenance.d.ts +78 -0
- package/dist/core/foundry/provenance.js +172 -0
- package/dist/core/foundry/provenance.js.map +1 -0
- package/dist/core/git.d.ts +13 -0
- package/dist/core/git.js +26 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +151 -0
- package/dist/core/inbox/merge.js +858 -0
- package/dist/core/inbox/merge.js.map +1 -0
- package/dist/core/inbox/store.js +10 -1
- package/dist/core/inbox/store.js.map +1 -1
- package/dist/core/integrations/markdown.d.ts +66 -0
- package/dist/core/integrations/markdown.js +132 -0
- package/dist/core/integrations/markdown.js.map +1 -0
- package/dist/core/integrations/secrets.d.ts +37 -0
- package/dist/core/integrations/secrets.js +67 -0
- package/dist/core/integrations/secrets.js.map +1 -0
- package/dist/core/integrations/stack.d.ts +47 -0
- package/dist/core/integrations/stack.js +107 -0
- package/dist/core/integrations/stack.js.map +1 -0
- package/dist/core/mcp-native-engineer.d.ts +123 -0
- package/dist/core/mcp-native-engineer.js +936 -0
- package/dist/core/mcp-native-engineer.js.map +1 -0
- package/dist/core/mcp-native.js +20 -42
- package/dist/core/mcp-native.js.map +1 -1
- package/dist/core/mcp-registry.js +2 -0
- package/dist/core/mcp-registry.js.map +1 -1
- package/dist/core/observability/codex-source.d.ts +53 -0
- package/dist/core/observability/codex-source.js +390 -0
- package/dist/core/observability/codex-source.js.map +1 -0
- package/dist/core/observability/estimate.d.ts +12 -0
- package/dist/core/observability/estimate.js +26 -0
- package/dist/core/observability/estimate.js.map +1 -1
- package/dist/core/observability/limits.d.ts +52 -0
- package/dist/core/observability/limits.js +316 -0
- package/dist/core/observability/limits.js.map +1 -0
- package/dist/core/observability/usage-source.js +3 -1
- package/dist/core/observability/usage-source.js.map +1 -1
- package/dist/core/portfolio/scanners.d.ts +1 -0
- package/dist/core/portfolio/scanners.js +40 -1
- package/dist/core/portfolio/scanners.js.map +1 -1
- package/dist/core/providers.js +4 -0
- package/dist/core/providers.js.map +1 -1
- package/dist/core/quality/health.js +1 -0
- package/dist/core/quality/health.js.map +1 -1
- package/dist/core/run/agent-loop.d.ts +8 -0
- package/dist/core/run/agent-loop.js +24 -14
- package/dist/core/run/agent-loop.js.map +1 -1
- package/dist/core/run/engine-registry.d.ts +48 -0
- package/dist/core/run/engine-registry.js +190 -0
- package/dist/core/run/engine-registry.js.map +1 -0
- package/dist/core/run/engines.d.ts +17 -3
- package/dist/core/run/engines.js +63 -44
- package/dist/core/run/engines.js.map +1 -1
- package/dist/core/run/learned-router.d.ts +121 -0
- package/dist/core/run/learned-router.js +374 -0
- package/dist/core/run/learned-router.js.map +1 -0
- package/dist/core/run/model-manager.js +3 -3
- package/dist/core/run/model-manager.js.map +1 -1
- package/dist/core/run/model-profile.d.ts +67 -0
- package/dist/core/run/model-profile.js +132 -0
- package/dist/core/run/model-profile.js.map +1 -0
- package/dist/core/run/orchestrator.d.ts +1 -1
- package/dist/core/run/orchestrator.js +642 -510
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/run/prompts/budget.d.ts +25 -0
- package/dist/core/run/prompts/budget.js +51 -0
- package/dist/core/run/prompts/budget.js.map +1 -0
- package/dist/core/run/prompts/index.d.ts +14 -0
- package/dist/core/run/prompts/index.js +59 -0
- package/dist/core/run/prompts/index.js.map +1 -0
- package/dist/core/run/prompts/layers.d.ts +26 -0
- package/dist/core/run/prompts/layers.js +73 -0
- package/dist/core/run/prompts/layers.js.map +1 -0
- package/dist/core/run/prompts/roles.d.ts +23 -0
- package/dist/core/run/prompts/roles.js +48 -0
- package/dist/core/run/prompts/roles.js.map +1 -0
- package/dist/core/run/prompts/types.d.ts +38 -0
- package/dist/core/run/prompts/types.js +5 -0
- package/dist/core/run/prompts/types.js.map +1 -0
- package/dist/core/run/provider-client.d.ts +6 -24
- package/dist/core/run/provider-client.js +125 -61
- package/dist/core/run/provider-client.js.map +1 -1
- package/dist/core/run/router.js +8 -0
- package/dist/core/run/router.js.map +1 -1
- package/dist/core/run/sandboxed-engine.d.ts +66 -0
- package/dist/core/run/sandboxed-engine.js +262 -0
- package/dist/core/run/sandboxed-engine.js.map +1 -0
- package/dist/core/run/verify-commands.d.ts +83 -0
- package/dist/core/run/verify-commands.js +234 -0
- package/dist/core/run/verify-commands.js.map +1 -0
- package/dist/core/run/verify.d.ts +31 -1
- package/dist/core/run/verify.js +48 -0
- package/dist/core/run/verify.js.map +1 -1
- package/dist/core/sandbox/audit.d.ts +27 -0
- package/dist/core/sandbox/audit.js +34 -0
- package/dist/core/sandbox/audit.js.map +1 -1
- package/dist/core/sandbox/confine.d.ts +134 -0
- package/dist/core/sandbox/confine.js +370 -0
- package/dist/core/sandbox/confine.js.map +1 -0
- package/dist/core/types.d.ts +227 -5
- package/dist/core/web/api.js +77 -28
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/control.d.ts +116 -0
- package/dist/core/web/control.js +307 -0
- package/dist/core/web/control.js.map +1 -0
- package/dist/core/web/public/app.js +512 -6
- package/dist/core/web/public/styles.css +468 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandboxed-engine.ts — M45: run an external agent CLI (Claude Code / Codex)
|
|
3
|
+
* INSIDE a throwaway git worktree and capture ONLY its diff as a PENDING inbox
|
|
4
|
+
* proposal. This is the keystone that lets the autonomous fleet drive frontier
|
|
5
|
+
* backends without ever touching the live tree.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY MODEL (external CLIs are black boxes):
|
|
8
|
+
* - Edits are confined to an M21 sandbox worktree (cwd = worktreePath). The
|
|
9
|
+
* worktree shares the source repo's .git, so it inherits remotes — therefore
|
|
10
|
+
* we sever git's PUSH credential channel (see buildContainedEnv): no token
|
|
11
|
+
* env vars, no ssh-agent, GIT_TERMINAL_PROMPT=0, and a per-invocation
|
|
12
|
+
* `core.hooksPath` (via GIT_CONFIG_* env — NO shared-config mutation) whose
|
|
13
|
+
* `pre-push` hook hard-fails every push.
|
|
14
|
+
* - The agent still authenticates to ITS OWN vendor (Claude/Codex subscription)
|
|
15
|
+
* via its on-disk session reached through the preserved real HOME — a
|
|
16
|
+
* different credential channel from git push, which we cut.
|
|
17
|
+
* - We consume ONLY the captured worktree diff. The agent's own commits land on
|
|
18
|
+
* the throwaway scratch branch and are discarded with the sandbox.
|
|
19
|
+
* - RESIDUAL (documented): the filesystem is not jailed — the agent can READ
|
|
20
|
+
* outside the worktree. True OS isolation (container/VM) is a later milestone.
|
|
21
|
+
*
|
|
22
|
+
* ENTIRELY GATED: only reached when cfg.foundry opts in (or opts.sandboxEngine).
|
|
23
|
+
* Default builtin-only behavior is unaffected.
|
|
24
|
+
*/
|
|
25
|
+
import type { AshlrConfig, EngineId, EngineTier, RunBudget, RunState, Sandbox } from '../types.js';
|
|
26
|
+
export interface SandboxedEngineResult {
|
|
27
|
+
/** Delegated RunState (status/usage/engineModel/engineTier). */
|
|
28
|
+
state: RunState;
|
|
29
|
+
/** Inbox proposal id when a non-empty diff was captured. */
|
|
30
|
+
proposalId?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RunEngineSandboxedOptions {
|
|
33
|
+
/** Absolute source repo the worktree forks from. */
|
|
34
|
+
sourceRepo: string;
|
|
35
|
+
/** Model id for the backend (else cfg.foundry.models[engine]). */
|
|
36
|
+
model?: string;
|
|
37
|
+
/** Budget hints recorded on the RunState. */
|
|
38
|
+
budget?: Partial<RunBudget>;
|
|
39
|
+
/** File the diff as a PENDING proposal (default true). */
|
|
40
|
+
propose?: boolean;
|
|
41
|
+
/** Reuse an existing worktree (e.g. the swarm's) instead of creating one. */
|
|
42
|
+
existingWorktree?: Sandbox;
|
|
43
|
+
/** Pre-generated run id (else one is generated). */
|
|
44
|
+
runId?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Trust tier of a backend. M50: read from the declarative engine registry (single
|
|
48
|
+
* source of truth) rather than a hardcoded set — so a config-added backend is
|
|
49
|
+
* tiered by its declared `tier`. An unknown engine is 'local' (never implicitly
|
|
50
|
+
* frontier). The builtin registry maps {claude, codex} → 'frontier', all else →
|
|
51
|
+
* 'local', identical to the pre-M50 FRONTIER_ENGINES behavior.
|
|
52
|
+
*/
|
|
53
|
+
export declare function engineTierOf(engine: EngineId, cfg?: AshlrConfig): EngineTier;
|
|
54
|
+
/**
|
|
55
|
+
* Build the contained env for an external agent subprocess. Preserves the agent's
|
|
56
|
+
* OWN auth (real HOME + its config-home overrides) while severing git push creds.
|
|
57
|
+
* `hooksDir` holds the pre-push blocker installed via per-invocation git config.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildContainedEnv(cfg: AshlrConfig, hooksDir: string): NodeJS.ProcessEnv;
|
|
60
|
+
/**
|
|
61
|
+
* Run `engine` on `goal` inside a sandbox worktree of `opts.sourceRepo`, capturing
|
|
62
|
+
* the diff as a PENDING proposal. Never throws; failures surface as a 'failed'
|
|
63
|
+
* RunState. Does NOT fall back to a raw (unsandboxed) run — that would defeat the
|
|
64
|
+
* containment, so a sandbox-creation failure is terminal here.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runEngineSandboxed(engine: EngineId, goal: string, cfg: AshlrConfig, opts: RunEngineSandboxedOptions): Promise<SandboxedEngineResult>;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandboxed-engine.ts — M45: run an external agent CLI (Claude Code / Codex)
|
|
3
|
+
* INSIDE a throwaway git worktree and capture ONLY its diff as a PENDING inbox
|
|
4
|
+
* proposal. This is the keystone that lets the autonomous fleet drive frontier
|
|
5
|
+
* backends without ever touching the live tree.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY MODEL (external CLIs are black boxes):
|
|
8
|
+
* - Edits are confined to an M21 sandbox worktree (cwd = worktreePath). The
|
|
9
|
+
* worktree shares the source repo's .git, so it inherits remotes — therefore
|
|
10
|
+
* we sever git's PUSH credential channel (see buildContainedEnv): no token
|
|
11
|
+
* env vars, no ssh-agent, GIT_TERMINAL_PROMPT=0, and a per-invocation
|
|
12
|
+
* `core.hooksPath` (via GIT_CONFIG_* env — NO shared-config mutation) whose
|
|
13
|
+
* `pre-push` hook hard-fails every push.
|
|
14
|
+
* - The agent still authenticates to ITS OWN vendor (Claude/Codex subscription)
|
|
15
|
+
* via its on-disk session reached through the preserved real HOME — a
|
|
16
|
+
* different credential channel from git push, which we cut.
|
|
17
|
+
* - We consume ONLY the captured worktree diff. The agent's own commits land on
|
|
18
|
+
* the throwaway scratch branch and are discarded with the sandbox.
|
|
19
|
+
* - RESIDUAL (documented): the filesystem is not jailed — the agent can READ
|
|
20
|
+
* outside the worktree. True OS isolation (container/VM) is a later milestone.
|
|
21
|
+
*
|
|
22
|
+
* ENTIRELY GATED: only reached when cfg.foundry opts in (or opts.sandboxEngine).
|
|
23
|
+
* Default builtin-only behavior is unaffected.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { buildEngineCommand, spawnEngine } from './engines.js';
|
|
29
|
+
import { resolveEngineSpec } from './engine-registry.js';
|
|
30
|
+
import { buildSandboxLauncher, confinementProfileFor } from '../sandbox/confine.js';
|
|
31
|
+
import { audit as auditConfinement } from '../sandbox/audit.js';
|
|
32
|
+
import { newUsage, estCostUsd } from './budget.js';
|
|
33
|
+
import { withToolEnv } from '../env-bridge.js';
|
|
34
|
+
import { scrubSecrets } from '../knowledge/index.js';
|
|
35
|
+
import { selectInboxStore } from '../seams/inbox.js';
|
|
36
|
+
import { hashDiff, signProvenance } from '../foundry/provenance.js';
|
|
37
|
+
/** Default hard wall-clock for an autonomous external run (20 min). */
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 20 * 60_000;
|
|
39
|
+
/** Credential-shaped env var names that must never reach the agent subprocess. */
|
|
40
|
+
const CRED_ENV_DENY = /(_|^)(TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIALS?)$/i;
|
|
41
|
+
/**
|
|
42
|
+
* Trust tier of a backend. M50: read from the declarative engine registry (single
|
|
43
|
+
* source of truth) rather than a hardcoded set — so a config-added backend is
|
|
44
|
+
* tiered by its declared `tier`. An unknown engine is 'local' (never implicitly
|
|
45
|
+
* frontier). The builtin registry maps {claude, codex} → 'frontier', all else →
|
|
46
|
+
* 'local', identical to the pre-M50 FRONTIER_ENGINES behavior.
|
|
47
|
+
*/
|
|
48
|
+
export function engineTierOf(engine, cfg) {
|
|
49
|
+
return resolveEngineSpec(engine, cfg)?.tier ?? 'local';
|
|
50
|
+
}
|
|
51
|
+
const PRE_PUSH_BLOCK = '#!/bin/sh\n' +
|
|
52
|
+
'# ashlr M45: pushes are forbidden from a sandboxed-engine worktree.\n' +
|
|
53
|
+
'echo "ashlr: git push is blocked inside the sandbox" >&2\n' +
|
|
54
|
+
'exit 1\n';
|
|
55
|
+
/**
|
|
56
|
+
* Build the contained env for an external agent subprocess. Preserves the agent's
|
|
57
|
+
* OWN auth (real HOME + its config-home overrides) while severing git push creds.
|
|
58
|
+
* `hooksDir` holds the pre-push blocker installed via per-invocation git config.
|
|
59
|
+
*/
|
|
60
|
+
export function buildContainedEnv(cfg, hooksDir) {
|
|
61
|
+
const realHome = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
62
|
+
const base = {};
|
|
63
|
+
base.PATH = process.env.PATH ?? process.env.Path ?? '';
|
|
64
|
+
if (realHome)
|
|
65
|
+
base.HOME = realHome;
|
|
66
|
+
base.LANG = process.env.LANG ?? 'C';
|
|
67
|
+
if (process.env.TERM)
|
|
68
|
+
base.TERM = process.env.TERM;
|
|
69
|
+
if (process.env.TMPDIR)
|
|
70
|
+
base.TMPDIR = process.env.TMPDIR;
|
|
71
|
+
if (process.platform === 'win32') {
|
|
72
|
+
if (realHome)
|
|
73
|
+
base.USERPROFILE = realHome;
|
|
74
|
+
for (const k of ['SystemRoot', 'windir', 'PATHEXT', 'COMSPEC', 'TEMP', 'TMP', 'APPDATA', 'LOCALAPPDATA']) {
|
|
75
|
+
const v = process.env[k];
|
|
76
|
+
if (v)
|
|
77
|
+
base[k] = v;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Preserve the agent CLIs' OWN config homes — their subscription auth lives here.
|
|
81
|
+
for (const k of ['CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME']) {
|
|
82
|
+
const v = process.env[k];
|
|
83
|
+
if (v)
|
|
84
|
+
base[k] = v;
|
|
85
|
+
}
|
|
86
|
+
// Layer ashlr non-secret config (OLLAMA_HOST, provider chain, paths, …).
|
|
87
|
+
const env = withToolEnv(cfg, base);
|
|
88
|
+
// Defensive: strip any credential-shaped var that slipped through.
|
|
89
|
+
for (const k of Object.keys(env)) {
|
|
90
|
+
if (CRED_ENV_DENY.test(k))
|
|
91
|
+
delete env[k];
|
|
92
|
+
}
|
|
93
|
+
// Sever git's PUSH credential channels (the agent's vendor auth via HOME is untouched).
|
|
94
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
95
|
+
env.GIT_ASKPASS = '';
|
|
96
|
+
env.SSH_ASKPASS = '';
|
|
97
|
+
env.GIT_SSH_COMMAND = 'ssh -oBatchMode=yes';
|
|
98
|
+
delete env.SSH_AUTH_SOCK;
|
|
99
|
+
env.GIT_CONFIG_NOSYSTEM = '1';
|
|
100
|
+
// Per-invocation core.hooksPath (no shared-config mutation) → pre-push blocker.
|
|
101
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
102
|
+
env.GIT_CONFIG_KEY_0 = 'core.hooksPath';
|
|
103
|
+
env.GIT_CONFIG_VALUE_0 = hooksDir;
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
/** Create a temp hooks dir containing a pre-push blocker. Returns its path. */
|
|
107
|
+
function installPrePushBlocker() {
|
|
108
|
+
const hooksDir = mkdtempSync(join(tmpdir(), 'ashlr-hooks-'));
|
|
109
|
+
writeFileSync(join(hooksDir, 'pre-push'), PRE_PUSH_BLOCK, { mode: 0o755 });
|
|
110
|
+
return hooksDir;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Run `engine` on `goal` inside a sandbox worktree of `opts.sourceRepo`, capturing
|
|
114
|
+
* the diff as a PENDING proposal. Never throws; failures surface as a 'failed'
|
|
115
|
+
* RunState. Does NOT fall back to a raw (unsandboxed) run — that would defeat the
|
|
116
|
+
* containment, so a sandbox-creation failure is terminal here.
|
|
117
|
+
*/
|
|
118
|
+
export async function runEngineSandboxed(engine, goal, cfg, opts) {
|
|
119
|
+
const model = opts.model ?? cfg.foundry?.models?.[engine];
|
|
120
|
+
const engineModel = `${engine}:${model ?? 'default'}`;
|
|
121
|
+
const tier = engineTierOf(engine, cfg);
|
|
122
|
+
const id = opts.runId ?? `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
123
|
+
const mk = (over) => ({
|
|
124
|
+
id,
|
|
125
|
+
goal,
|
|
126
|
+
engine,
|
|
127
|
+
provider: 'external',
|
|
128
|
+
engineModel,
|
|
129
|
+
engineTier: tier,
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
updatedAt: new Date().toISOString(),
|
|
132
|
+
budget: {
|
|
133
|
+
maxTokens: opts.budget?.maxTokens ?? 0,
|
|
134
|
+
maxSteps: opts.budget?.maxSteps ?? 0,
|
|
135
|
+
allowCloud: opts.budget?.allowCloud ?? false,
|
|
136
|
+
},
|
|
137
|
+
usage: newUsage(),
|
|
138
|
+
tasks: [],
|
|
139
|
+
steps: [],
|
|
140
|
+
status: 'running',
|
|
141
|
+
...over,
|
|
142
|
+
});
|
|
143
|
+
const wt = await import('../sandbox/worktree.js');
|
|
144
|
+
// Acquire a worktree (reuse the caller's when provided).
|
|
145
|
+
let sb;
|
|
146
|
+
let createdHere = false;
|
|
147
|
+
if (opts.existingWorktree) {
|
|
148
|
+
sb = opts.existingWorktree;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
try {
|
|
152
|
+
sb = wt.createSandbox(opts.sourceRepo);
|
|
153
|
+
createdHere = true;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
return { state: mk({ status: 'failed', result: `sandbox unavailable: ${msg}` }) };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const hooksDir = installPrePushBlocker();
|
|
161
|
+
const env = buildContainedEnv(cfg, hooksDir);
|
|
162
|
+
let proposalId;
|
|
163
|
+
try {
|
|
164
|
+
const cmd = buildEngineCommand(engine, goal, cfg, {
|
|
165
|
+
cwd: sb.worktreePath,
|
|
166
|
+
model,
|
|
167
|
+
autonomous: true,
|
|
168
|
+
});
|
|
169
|
+
if (!cmd) {
|
|
170
|
+
return { state: mk({ status: 'failed', result: `no command for engine "${engine}"` }) };
|
|
171
|
+
}
|
|
172
|
+
// M52: compute the OS-level sandbox launcher for this engine.
|
|
173
|
+
// confinementProfileFor is pure; buildSandboxLauncher may throw when
|
|
174
|
+
// onUnsupported:'fail' and the platform has no jail binary — that
|
|
175
|
+
// propagates as a failed run (caught in spawnEngine's never-throw wrapper).
|
|
176
|
+
// buildContainedEnv, the pre-push hook, and the diff/provenance logic are
|
|
177
|
+
// all unchanged — the launcher wraps the final spawn only.
|
|
178
|
+
const confinementProfile = confinementProfileFor(engine, cfg);
|
|
179
|
+
const launcher = buildSandboxLauncher(confinementProfile, {
|
|
180
|
+
worktree: sb.worktreePath,
|
|
181
|
+
home: process.env.HOME ?? process.env.USERPROFILE,
|
|
182
|
+
env: env,
|
|
183
|
+
});
|
|
184
|
+
// Emit confinement audit event (append-only, never throws).
|
|
185
|
+
auditConfinement({
|
|
186
|
+
action: 'confinement.run',
|
|
187
|
+
repo: sb.worktreePath,
|
|
188
|
+
sandboxId: sb.id,
|
|
189
|
+
summary: `engine=${engine} mode=${confinementProfile.mode ?? 'off'} platform=${process.platform} launched=${launcher !== null} networkEgress=${confinementProfile.networkEgress ?? false}`,
|
|
190
|
+
result: 'ok',
|
|
191
|
+
});
|
|
192
|
+
const res = spawnEngine(cmd, cfg, {
|
|
193
|
+
env,
|
|
194
|
+
timeoutMs: cfg.foundry?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
195
|
+
launcher: launcher ?? undefined,
|
|
196
|
+
});
|
|
197
|
+
const usage = res.usage
|
|
198
|
+
? {
|
|
199
|
+
tokensIn: res.usage.tokensIn,
|
|
200
|
+
tokensOut: res.usage.tokensOut,
|
|
201
|
+
steps: 1,
|
|
202
|
+
estCostUsd: estCostUsd(engine, res.usage.tokensIn, res.usage.tokensOut),
|
|
203
|
+
}
|
|
204
|
+
: newUsage();
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
return {
|
|
207
|
+
state: mk({ status: 'failed', result: `engine "${engine}" failed: ${res.error ?? 'unknown error'}`, usage }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Capture the worktree diff (best-effort) and file it as a PENDING proposal.
|
|
211
|
+
if (opts.propose !== false) {
|
|
212
|
+
try {
|
|
213
|
+
const diff = wt.sandboxDiff(sb);
|
|
214
|
+
if (diff.files > 0 && diff.patch.trim().length > 0) {
|
|
215
|
+
// M47.1 (H3): scrub ONCE and reuse the same scrubbed string for BOTH
|
|
216
|
+
// the stored diff and the signed hash — so the merge gate recomputes
|
|
217
|
+
// an identical hash. Bind {engineModel, tier, diffHash} with an HMAC
|
|
218
|
+
// so a forged on-disk record cannot claim frontier merge-authority.
|
|
219
|
+
const scrubbed = scrubSecrets(diff.patch);
|
|
220
|
+
const diffHash = hashDiff(scrubbed);
|
|
221
|
+
const provenanceSig = signProvenance(engineModel, tier, diffHash);
|
|
222
|
+
const proposal = selectInboxStore(cfg).create({
|
|
223
|
+
repo: sb.sourceRepo,
|
|
224
|
+
origin: 'agent',
|
|
225
|
+
kind: 'patch',
|
|
226
|
+
title: `${engine} run: ${goal.slice(0, 80)}`,
|
|
227
|
+
summary: `Sandboxed ${engineModel} run produced ${diff.files} file(s) ` +
|
|
228
|
+
`(+${diff.insertions}/-${diff.deletions}). Review before applying.`,
|
|
229
|
+
diff: scrubbed,
|
|
230
|
+
diffHash,
|
|
231
|
+
provenanceSig,
|
|
232
|
+
sandboxId: sb.id,
|
|
233
|
+
engineModel,
|
|
234
|
+
engineTier: tier,
|
|
235
|
+
});
|
|
236
|
+
proposalId = proposal.id;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// diff/proposal capture is best-effort — never fail the run on it.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { state: mk({ status: 'done', result: res.output, usage }), proposalId };
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
try {
|
|
247
|
+
rmSync(hooksDir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// best-effort cleanup
|
|
251
|
+
}
|
|
252
|
+
if (createdHere) {
|
|
253
|
+
try {
|
|
254
|
+
wt.removeSandbox(sb);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// removal is idempotent
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=sandboxed-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandboxed-engine.js","sourceRoot":"","sources":["../../../src/core/run/sandboxed-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAUjC,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EAAE,KAAK,IAAI,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAwBpE,uEAAuE;AACvE,MAAM,kBAAkB,GAAG,EAAE,GAAG,MAAM,CAAC;AAEvC,kFAAkF;AAClF,MAAM,aAAa,GAAG,wDAAwD,CAAC;AAE/E;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAAgB,EAAE,GAAiB;IAC9D,OAAO,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,IAAI,OAAO,CAAC;AACzD,CAAC;AAED,MAAM,cAAc,GAClB,aAAa;IACb,uEAAuE;IACvE,4DAA4D;IAC5D,UAAU,CAAC;AAEb;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAgB,EAAE,QAAgB;IAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;IACnE,MAAM,IAAI,GAAsB,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACvD,IAAI,QAAQ;QAAE,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC;IACnC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC;IACpC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI;QAAE,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IACnD,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;IAEzD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,IAAI,QAAQ;YAAE,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC;QAC1C,KAAK,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,CAAC;YACzG,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC;gBAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,kFAAkF;IAClF,KAAK,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,eAAe,CAAC,EAAE,CAAC;QACxF,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC;YAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,yEAAyE;IACzE,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAEnC,mEAAmE;IACnE,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,wFAAwF;IACxF,GAAG,CAAC,mBAAmB,GAAG,GAAG,CAAC;IAC9B,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC;IACrB,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC;IACrB,GAAG,CAAC,eAAe,GAAG,qBAAqB,CAAC;IAC5C,OAAO,GAAG,CAAC,aAAa,CAAC;IACzB,GAAG,CAAC,mBAAmB,GAAG,GAAG,CAAC;IAC9B,gFAAgF;IAChF,GAAG,CAAC,gBAAgB,GAAG,GAAG,CAAC;IAC3B,GAAG,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IACxC,GAAG,CAAC,kBAAkB,GAAG,QAAQ,CAAC;IAClC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+EAA+E;AAC/E,SAAS,qBAAqB;IAC5B,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IAC7D,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3E,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAgB,EAChB,IAAY,EACZ,GAAgB,EAChB,IAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,GAAG,MAAM,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;IACtD,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,MAAM,EAAE,GACN,IAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAE3F,MAAM,EAAE,GAAG,CAAC,IAAuB,EAAY,EAAE,CAAC,CAAC;QACjD,EAAE;QACF,IAAI;QACJ,MAAM;QACN,QAAQ,EAAE,UAAU;QACpB,WAAW;QACX,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,MAAM,EAAE;YACN,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,IAAI,CAAC;YACtC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,CAAC;YACpC,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,IAAI,KAAK;SAC7C;QACD,KAAK,EAAE,QAAQ,EAAE;QACjB,KAAK,EAAE,EAAE;QACT,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,SAAS;QACjB,GAAG,IAAI;KACR,CAAC,CAAC;IAEH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,CAAC;IAElD,yDAAyD;IACzD,IAAI,EAAW,CAAC;IAChB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC;IAC7B,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACvC,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,wBAAwB,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;QACpF,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;IACzC,MAAM,GAAG,GAAG,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC7C,IAAI,UAA8B,CAAC;IAEnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE;YAChD,GAAG,EAAE,EAAE,CAAC,YAAY;YACpB,KAAK;YACL,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,0BAA0B,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC;QAC1F,CAAC;QAED,8DAA8D;QAC9D,qEAAqE;QACrE,kEAAkE;QAClE,4EAA4E;QAC5E,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAM,kBAAkB,GAAG,qBAAqB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,oBAAoB,CAAC,kBAAkB,EAAE;YACxD,QAAQ,EAAE,EAAE,CAAC,YAAY;YACzB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW;YACjD,GAAG,EAAE,GAAG;SACT,CAAC,CAAC;QAEH,4DAA4D;QAC5D,gBAAgB,CAAC;YACf,MAAM,EAAE,iBAAiB;YACzB,IAAI,EAAE,EAAE,CAAC,YAAY;YACrB,SAAS,EAAE,EAAE,CAAC,EAAE;YAChB,OAAO,EAAE,UAAU,MAAM,SAAS,kBAAkB,CAAC,IAAI,IAAI,KAAK,aAAa,OAAO,CAAC,QAAQ,aAAa,QAAQ,KAAK,IAAI,kBAAkB,kBAAkB,CAAC,aAAa,IAAI,KAAK,EAAE;YAC1L,MAAM,EAAE,IAAI;SACb,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;YAChC,GAAG;YACH,SAAS,EAAE,GAAG,CAAC,OAAO,EAAE,SAAS,IAAI,kBAAkB;YACvD,QAAQ,EAAE,QAAQ,IAAI,SAAS;SAChC,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK;YACrB,CAAC,CAAC;gBACE,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;gBAC5B,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS;gBAC9B,KAAK,EAAE,CAAC;gBACR,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;aACxE;YACH,CAAC,CAAC,QAAQ,EAAE,CAAC;QAEf,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO;gBACL,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,MAAM,aAAa,GAAG,CAAC,KAAK,IAAI,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC;aAC7G,CAAC;QACJ,CAAC;QAED,6EAA6E;QAC7E,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;gBAChC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACnD,qEAAqE;oBACrE,qEAAqE;oBACrE,qEAAqE;oBACrE,oEAAoE;oBACpE,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAC1C,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;oBACpC,MAAM,aAAa,GAAG,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;oBAClE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;wBAC5C,IAAI,EAAE,EAAE,CAAC,UAAU;wBACnB,MAAM,EAAE,OAAO;wBACf,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE,GAAG,MAAM,SAAS,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE;wBAC5C,OAAO,EACL,aAAa,WAAW,iBAAiB,IAAI,CAAC,KAAK,WAAW;4BAC9D,KAAK,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,SAAS,4BAA4B;wBACrE,IAAI,EAAE,QAAQ;wBACd,QAAQ;wBACR,aAAa;wBACb,SAAS,EAAE,EAAE,CAAC,EAAE;wBAChB,WAAW;wBACX,UAAU,EAAE,IAAI;qBACjB,CAAC,CAAC;oBACH,UAAU,GAAG,QAAQ,CAAC,EAAE,CAAC;gBAC3B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,mEAAmE;YACrE,CAAC;QACH,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;IAClF,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;QACD,IAAI,WAAW,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M43: Structured verification primitives — detect & run typecheck/test/lint.
|
|
3
|
+
*
|
|
4
|
+
* The hub runs local-model tasks (`runTask`). After a task completes, M43 runs a
|
|
5
|
+
* STRUCTURED verification (typecheck/test/lint). This module provides the two
|
|
6
|
+
* low-level halves the orchestrator's repair loop wires together:
|
|
7
|
+
*
|
|
8
|
+
* detectVerifyCommands() — READ-ONLY introspection of a workspace to decide
|
|
9
|
+
* which verification commands exist (never installs, never mutates).
|
|
10
|
+
* runVerifyCommand() — runs ONE detected command in the workspace with a
|
|
11
|
+
* hard timeout and arg arrays only; captures + scrubs + caps output. Uses a
|
|
12
|
+
* shell ONLY on Windows so PATHEXT can resolve npm.cmd/npx.cmd shims.
|
|
13
|
+
*
|
|
14
|
+
* Rules (mirroring git.ts):
|
|
15
|
+
* - Node builtins only; zero third-party deps.
|
|
16
|
+
* - Subprocesses run with execFileSync / spawnSync via ARG ARRAYS, NO shell,
|
|
17
|
+
* and a tight timeout so a hung verify can never block the orchestrator.
|
|
18
|
+
* - runVerifyCommand never throws — any failure resolves to { ok:false } with
|
|
19
|
+
* the error captured in `output`.
|
|
20
|
+
*/
|
|
21
|
+
import type { SpawnSyncOptionsWithStringEncoding } from 'node:child_process';
|
|
22
|
+
import type { AshlrConfig } from '../types.js';
|
|
23
|
+
/** A single verification command to run, classified by what it checks. */
|
|
24
|
+
export interface VerifyCommand {
|
|
25
|
+
kind: 'typecheck' | 'test' | 'lint';
|
|
26
|
+
/** Exact argv (executable + args). NEVER passed through a shell. */
|
|
27
|
+
cmd: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Outcome of running one VerifyCommand. */
|
|
30
|
+
export interface VerifyCommandResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** Human-readable command (the argv joined) for logs / verdicts. */
|
|
33
|
+
command: string;
|
|
34
|
+
exitCode: number;
|
|
35
|
+
/** Combined stdout+stderr, secret-scrubbed and size-capped (≤32KB). */
|
|
36
|
+
output: string;
|
|
37
|
+
timedOut: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* READ-ONLY detection of the verification commands available in `workspaceRoot`.
|
|
41
|
+
* Never installs, never spawns — purely inspects package.json + config files.
|
|
42
|
+
*
|
|
43
|
+
* Preference order per kind:
|
|
44
|
+
* typecheck: package.json `typecheck` script → else `tsc --noEmit` when a
|
|
45
|
+
* tsconfig.json exists.
|
|
46
|
+
* test: package.json `test` script → else `vitest run` when vitest is a
|
|
47
|
+
* dep or a vitest.config.* exists.
|
|
48
|
+
* lint: package.json `lint` script (no fallback).
|
|
49
|
+
*
|
|
50
|
+
* Scripts run through the detected package manager (`<pm> run <script>`);
|
|
51
|
+
* fallbacks run through `npx`. Returns [] when nothing is detected — the caller
|
|
52
|
+
* treats an empty list as a no-op.
|
|
53
|
+
*/
|
|
54
|
+
export declare function detectVerifyCommands(workspaceRoot: string): VerifyCommand[];
|
|
55
|
+
/**
|
|
56
|
+
* Build the spawnSync options for a verify command, made cross-platform.
|
|
57
|
+
*
|
|
58
|
+
* On Windows the package-manager runners we spawn (`npm`, `npx`, `pnpm`,
|
|
59
|
+
* `yarn`, `bun`) are not real executables but `.cmd`/`.ps1` shims on PATH.
|
|
60
|
+
* `spawnSync` does NOT consult PATHEXT, so `spawnSync('npm', …)` fails with
|
|
61
|
+
* ENOENT and the command silently does nothing. Running through the shell
|
|
62
|
+
* (`shell: true`) lets the OS resolve `npm.cmd`/`npx.cmd` etc. via PATHEXT.
|
|
63
|
+
* Our argv arrays are simple, space-free tokens (e.g. ['npm','run','typecheck'],
|
|
64
|
+
* ['npx','tsc','--noEmit']), so shell quoting is a non-issue. `windowsHide`
|
|
65
|
+
* keeps a console window from flashing up. (git/node/tsc are real binaries and
|
|
66
|
+
* resolve fine either way; the shell route is harmless for them.)
|
|
67
|
+
*
|
|
68
|
+
* `platform` is injected (defaults to `process.platform`) purely so this can be
|
|
69
|
+
* unit-tested without switching the host OS.
|
|
70
|
+
*/
|
|
71
|
+
export declare function spawnOptionsFor(workspaceRoot: string, timeout: number, platform?: NodeJS.Platform): SpawnSyncOptionsWithStringEncoding;
|
|
72
|
+
/**
|
|
73
|
+
* Run ONE verification command in `workspaceRoot` (cwd). Arg array, shell only
|
|
74
|
+
* on Windows (see spawnOptionsFor), with a hard timeout (default 120s, capped at
|
|
75
|
+
* 600s). Captures stdout+stderr,
|
|
76
|
+
* scrubs + caps via renderToolText, and audits the outcome.
|
|
77
|
+
*
|
|
78
|
+
* Never throws — a spawn failure or timeout resolves to { ok:false } with the
|
|
79
|
+
* error in `output`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function runVerifyCommand(vc: VerifyCommand, workspaceRoot: string, _cfg: AshlrConfig, opts?: {
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
}): VerifyCommandResult;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M43: Structured verification primitives — detect & run typecheck/test/lint.
|
|
3
|
+
*
|
|
4
|
+
* The hub runs local-model tasks (`runTask`). After a task completes, M43 runs a
|
|
5
|
+
* STRUCTURED verification (typecheck/test/lint). This module provides the two
|
|
6
|
+
* low-level halves the orchestrator's repair loop wires together:
|
|
7
|
+
*
|
|
8
|
+
* detectVerifyCommands() — READ-ONLY introspection of a workspace to decide
|
|
9
|
+
* which verification commands exist (never installs, never mutates).
|
|
10
|
+
* runVerifyCommand() — runs ONE detected command in the workspace with a
|
|
11
|
+
* hard timeout and arg arrays only; captures + scrubs + caps output. Uses a
|
|
12
|
+
* shell ONLY on Windows so PATHEXT can resolve npm.cmd/npx.cmd shims.
|
|
13
|
+
*
|
|
14
|
+
* Rules (mirroring git.ts):
|
|
15
|
+
* - Node builtins only; zero third-party deps.
|
|
16
|
+
* - Subprocesses run with execFileSync / spawnSync via ARG ARRAYS, NO shell,
|
|
17
|
+
* and a tight timeout so a hung verify can never block the orchestrator.
|
|
18
|
+
* - runVerifyCommand never throws — any failure resolves to { ok:false } with
|
|
19
|
+
* the error captured in `output`.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { renderToolText } from '../mcp-native.js';
|
|
25
|
+
import { audit } from '../sandbox/audit.js';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/** Default per-command timeout (ms). */
|
|
30
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
31
|
+
/** Hard ceiling on the per-command timeout (ms). */
|
|
32
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Internal helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Detect the package-manager runner from the lockfile present in `root`.
|
|
38
|
+
* Order matters only insofar as a repo should have exactly one lockfile;
|
|
39
|
+
* pnpm → yarn → bun → npm (default).
|
|
40
|
+
*/
|
|
41
|
+
function detectPackageManager(root) {
|
|
42
|
+
if (existsSync(join(root, 'pnpm-lock.yaml')))
|
|
43
|
+
return 'pnpm';
|
|
44
|
+
if (existsSync(join(root, 'yarn.lock')))
|
|
45
|
+
return 'yarn';
|
|
46
|
+
if (existsSync(join(root, 'bun.lockb')))
|
|
47
|
+
return 'bun';
|
|
48
|
+
return 'npm';
|
|
49
|
+
}
|
|
50
|
+
/** Build the argv to run a package.json script named `script` via `pm`. */
|
|
51
|
+
function runScriptArgv(pm, script) {
|
|
52
|
+
// npm/pnpm/yarn/bun all accept `<pm> run <script>`.
|
|
53
|
+
return [pm, 'run', script];
|
|
54
|
+
}
|
|
55
|
+
/** Read & parse package.json once; returns the parsed object or null on any error. */
|
|
56
|
+
function readPackageJson(root) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(join(root, 'package.json'), 'utf8');
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Extract the string-valued scripts map from a parsed package.json; {} when absent. */
|
|
66
|
+
function scriptsOf(pkg) {
|
|
67
|
+
const scripts = pkg?.scripts;
|
|
68
|
+
if (!scripts || typeof scripts !== 'object')
|
|
69
|
+
return {};
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [k, v] of Object.entries(scripts)) {
|
|
72
|
+
if (typeof v === 'string')
|
|
73
|
+
out[k] = v;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** True when the parsed package.json declares `dep` in deps or devDeps. */
|
|
78
|
+
function hasDep(pkg, dep) {
|
|
79
|
+
if (!pkg)
|
|
80
|
+
return false;
|
|
81
|
+
return ((pkg.dependencies !== undefined && dep in pkg.dependencies) ||
|
|
82
|
+
(pkg.devDependencies !== undefined && dep in pkg.devDependencies));
|
|
83
|
+
}
|
|
84
|
+
/** True when a file matching `<prefix>.<ext>` exists for any common config ext. */
|
|
85
|
+
function hasConfigFile(root, prefix) {
|
|
86
|
+
try {
|
|
87
|
+
const entries = readdirSync(root);
|
|
88
|
+
return entries.some((f) => f === prefix || f.startsWith(`${prefix}.`));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Public: detectVerifyCommands
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* READ-ONLY detection of the verification commands available in `workspaceRoot`.
|
|
99
|
+
* Never installs, never spawns — purely inspects package.json + config files.
|
|
100
|
+
*
|
|
101
|
+
* Preference order per kind:
|
|
102
|
+
* typecheck: package.json `typecheck` script → else `tsc --noEmit` when a
|
|
103
|
+
* tsconfig.json exists.
|
|
104
|
+
* test: package.json `test` script → else `vitest run` when vitest is a
|
|
105
|
+
* dep or a vitest.config.* exists.
|
|
106
|
+
* lint: package.json `lint` script (no fallback).
|
|
107
|
+
*
|
|
108
|
+
* Scripts run through the detected package manager (`<pm> run <script>`);
|
|
109
|
+
* fallbacks run through `npx`. Returns [] when nothing is detected — the caller
|
|
110
|
+
* treats an empty list as a no-op.
|
|
111
|
+
*/
|
|
112
|
+
export function detectVerifyCommands(workspaceRoot) {
|
|
113
|
+
const commands = [];
|
|
114
|
+
const pkg = readPackageJson(workspaceRoot);
|
|
115
|
+
const scripts = scriptsOf(pkg);
|
|
116
|
+
const pm = detectPackageManager(workspaceRoot);
|
|
117
|
+
// --- typecheck ---
|
|
118
|
+
if (scripts['typecheck']) {
|
|
119
|
+
commands.push({ kind: 'typecheck', cmd: runScriptArgv(pm, 'typecheck') });
|
|
120
|
+
}
|
|
121
|
+
else if (existsSync(join(workspaceRoot, 'tsconfig.json'))) {
|
|
122
|
+
commands.push({ kind: 'typecheck', cmd: ['npx', 'tsc', '--noEmit'] });
|
|
123
|
+
}
|
|
124
|
+
// --- test ---
|
|
125
|
+
if (scripts['test']) {
|
|
126
|
+
commands.push({ kind: 'test', cmd: runScriptArgv(pm, 'test') });
|
|
127
|
+
}
|
|
128
|
+
else if (hasDep(pkg, 'vitest') || hasConfigFile(workspaceRoot, 'vitest.config')) {
|
|
129
|
+
commands.push({ kind: 'test', cmd: ['npx', 'vitest', 'run'] });
|
|
130
|
+
}
|
|
131
|
+
// --- lint ---
|
|
132
|
+
if (scripts['lint']) {
|
|
133
|
+
commands.push({ kind: 'lint', cmd: runScriptArgv(pm, 'lint') });
|
|
134
|
+
}
|
|
135
|
+
return commands;
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Public: spawnOptionsFor
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/**
|
|
141
|
+
* Build the spawnSync options for a verify command, made cross-platform.
|
|
142
|
+
*
|
|
143
|
+
* On Windows the package-manager runners we spawn (`npm`, `npx`, `pnpm`,
|
|
144
|
+
* `yarn`, `bun`) are not real executables but `.cmd`/`.ps1` shims on PATH.
|
|
145
|
+
* `spawnSync` does NOT consult PATHEXT, so `spawnSync('npm', …)` fails with
|
|
146
|
+
* ENOENT and the command silently does nothing. Running through the shell
|
|
147
|
+
* (`shell: true`) lets the OS resolve `npm.cmd`/`npx.cmd` etc. via PATHEXT.
|
|
148
|
+
* Our argv arrays are simple, space-free tokens (e.g. ['npm','run','typecheck'],
|
|
149
|
+
* ['npx','tsc','--noEmit']), so shell quoting is a non-issue. `windowsHide`
|
|
150
|
+
* keeps a console window from flashing up. (git/node/tsc are real binaries and
|
|
151
|
+
* resolve fine either way; the shell route is harmless for them.)
|
|
152
|
+
*
|
|
153
|
+
* `platform` is injected (defaults to `process.platform`) purely so this can be
|
|
154
|
+
* unit-tested without switching the host OS.
|
|
155
|
+
*/
|
|
156
|
+
export function spawnOptionsFor(workspaceRoot, timeout, platform = process.platform) {
|
|
157
|
+
const isWin = platform === 'win32';
|
|
158
|
+
return {
|
|
159
|
+
cwd: workspaceRoot,
|
|
160
|
+
timeout,
|
|
161
|
+
stdio: 'pipe',
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
shell: isWin, // resolve npm.cmd/npx.cmd etc. via PATHEXT on Windows
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Public: runVerifyCommand
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Run ONE verification command in `workspaceRoot` (cwd). Arg array, shell only
|
|
172
|
+
* on Windows (see spawnOptionsFor), with a hard timeout (default 120s, capped at
|
|
173
|
+
* 600s). Captures stdout+stderr,
|
|
174
|
+
* scrubs + caps via renderToolText, and audits the outcome.
|
|
175
|
+
*
|
|
176
|
+
* Never throws — a spawn failure or timeout resolves to { ok:false } with the
|
|
177
|
+
* error in `output`.
|
|
178
|
+
*/
|
|
179
|
+
export function runVerifyCommand(vc, workspaceRoot, _cfg, opts) {
|
|
180
|
+
const command = vc.cmd.join(' ');
|
|
181
|
+
const timeout = Math.min(MAX_TIMEOUT_MS, Math.max(1, opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
|
182
|
+
const bin = vc.cmd[0];
|
|
183
|
+
if (!bin) {
|
|
184
|
+
const output = renderToolText(`verify command "${vc.kind}" has an empty argv`);
|
|
185
|
+
audit({
|
|
186
|
+
action: 'verify:command',
|
|
187
|
+
repo: workspaceRoot,
|
|
188
|
+
sandboxId: null,
|
|
189
|
+
summary: `${vc.kind}: empty argv`,
|
|
190
|
+
result: 'error',
|
|
191
|
+
});
|
|
192
|
+
return { ok: false, command, exitCode: -1, output, timedOut: false };
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const res = spawnSync(bin, vc.cmd.slice(1), spawnOptionsFor(workspaceRoot, timeout));
|
|
196
|
+
const timedOut = res.error !== undefined && res.error.code === 'ETIMEDOUT';
|
|
197
|
+
// spawn-level failure (binary not found, timeout, …): res.status is null.
|
|
198
|
+
if (res.error) {
|
|
199
|
+
const output = renderToolText(`${command}\n${(res.stdout ?? '')}${res.stderr ?? ''}\n${res.error.message}`);
|
|
200
|
+
audit({
|
|
201
|
+
action: 'verify:command',
|
|
202
|
+
repo: workspaceRoot,
|
|
203
|
+
sandboxId: null,
|
|
204
|
+
summary: `${vc.kind}: ${command} → ${timedOut ? 'timed out' : 'spawn error'}`,
|
|
205
|
+
result: 'error',
|
|
206
|
+
});
|
|
207
|
+
return { ok: false, command, exitCode: -1, output, timedOut };
|
|
208
|
+
}
|
|
209
|
+
const exitCode = res.status ?? -1;
|
|
210
|
+
const ok = exitCode === 0;
|
|
211
|
+
const output = renderToolText(`${(res.stdout ?? '')}${res.stderr ?? ''}`);
|
|
212
|
+
audit({
|
|
213
|
+
action: 'verify:command',
|
|
214
|
+
repo: workspaceRoot,
|
|
215
|
+
sandboxId: null,
|
|
216
|
+
summary: `${vc.kind}: ${command} → exit ${exitCode}`,
|
|
217
|
+
result: ok ? 'ok' : 'error',
|
|
218
|
+
});
|
|
219
|
+
return { ok, command, exitCode, output, timedOut: false };
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
const output = renderToolText(`${command}\n${msg}`);
|
|
224
|
+
audit({
|
|
225
|
+
action: 'verify:command',
|
|
226
|
+
repo: workspaceRoot,
|
|
227
|
+
sandboxId: null,
|
|
228
|
+
summary: `${vc.kind}: ${command} → threw: ${msg}`,
|
|
229
|
+
result: 'error',
|
|
230
|
+
});
|
|
231
|
+
return { ok: false, command, exitCode: -1, output, timedOut: false };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=verify-commands.js.map
|