@agilno-tech/rivet 0.1.0-alpha.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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { serializeProtocolLaunch } from '../protocols/presentation.js';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { lstat, open, realpath } from 'node:fs/promises';
|
|
4
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { AgentContractError, containsSecretMaterial, failAgent, immutableJson } from './contract.js';
|
|
7
|
+
import { validateLaunchPayload } from '../prompts/launch-contract.js';
|
|
8
|
+
import { parsePlanningResult, validatePlanningPayload } from '../prompts/planning-contract.js';
|
|
9
|
+
|
|
10
|
+
const CONFIG_KEYS = new Set(['executable', 'interpreter', 'worktree', 'worktreeIdentity', 'environment', 'signal', 'launchTimeoutMs', 'timeoutMs', 'termGraceMs', 'killGraceMs', 'maxOutputBytes', 'maxInputBytes', 'allowOptionArgs', 'resultFormat']);
|
|
11
|
+
const REQUEST_KEYS = new Set(['args', 'cwd', 'payload', 'signal', 'protocolContext']);
|
|
12
|
+
// Keep the provider environment narrow, but preserve the non-secret account
|
|
13
|
+
// context required by native macOS clients to resolve their local login
|
|
14
|
+
// (Claude Code uses HOME and the shell identity variables when consulting its
|
|
15
|
+
// Keychain-backed session). Credential/token variables remain excluded below.
|
|
16
|
+
const SAFE_ENV = new Set(['LANG', 'LC_ALL', 'TZ', 'TERM', 'TMPDIR', 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL']);
|
|
17
|
+
const SENSITIVE_ENV = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|PRIVATE_KEY|KEY)(?:_|$)/i;
|
|
18
|
+
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
19
|
+
|
|
20
|
+
export function assertSupportedClientPlatform(platform = process.platform) {
|
|
21
|
+
if (platform !== 'darwin' && platform !== 'linux') failAgent('unsupported-platform');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function capture(input, keys, required, reason) {
|
|
25
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) failAgent(reason);
|
|
26
|
+
let ownKeys;
|
|
27
|
+
try { ownKeys = Reflect.ownKeys(input); } catch { failAgent(reason); }
|
|
28
|
+
if (ownKeys.some(key => typeof key !== 'string' || !keys.has(key))) failAgent(reason);
|
|
29
|
+
const output = Object.create(null);
|
|
30
|
+
try {
|
|
31
|
+
for (const key of ownKeys) {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
33
|
+
if (!descriptor?.enumerable) failAgent(reason);
|
|
34
|
+
output[key] = input[key];
|
|
35
|
+
}
|
|
36
|
+
} catch { failAgent(reason); }
|
|
37
|
+
if (required.some(key => !Object.hasOwn(output, key))) failAgent(reason);
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function boundedInteger(value, fallback, maximum, reason = 'invalid-contract') {
|
|
42
|
+
const result = value ?? fallback;
|
|
43
|
+
if (!Number.isSafeInteger(result) || result <= 0 || result > maximum) failAgent(reason);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function captureSignal(value, reason = 'invalid-contract') {
|
|
48
|
+
try {
|
|
49
|
+
if (value !== undefined && (!(value instanceof AbortSignal) || Object.getPrototypeOf(value) !== AbortSignal.prototype)) failAgent(reason);
|
|
50
|
+
return value;
|
|
51
|
+
} catch { failAgent(reason); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function signalAborted(signal, reason = 'invalid-contract') {
|
|
55
|
+
if (!signal) return false;
|
|
56
|
+
try { return signal.aborted === true; } catch { failAgent(reason); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function watchSignal(signal, onAbort, reason = 'invalid-contract') {
|
|
60
|
+
if (!signal) return () => {};
|
|
61
|
+
let installed = false;
|
|
62
|
+
try {
|
|
63
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
64
|
+
installed = true;
|
|
65
|
+
if (signal.aborted) onAbort();
|
|
66
|
+
} catch {
|
|
67
|
+
if (installed) {
|
|
68
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
69
|
+
}
|
|
70
|
+
failAgent(reason);
|
|
71
|
+
}
|
|
72
|
+
return () => {
|
|
73
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino; }
|
|
78
|
+
function identity(metadata) { return Object.freeze({ dev: metadata.dev.toString(), ino: metadata.ino.toString() }); }
|
|
79
|
+
|
|
80
|
+
async function verifyExecutable(path, expected, guard = () => {}) {
|
|
81
|
+
if (typeof path !== 'string' || path.length < 2 || path.length > 1024 || !isAbsolute(path) || resolve(path) !== path || /[\u0000\r\n]/.test(path)) failAgent('executable-unsafe');
|
|
82
|
+
try {
|
|
83
|
+
const before = await lstat(path, { bigint: true });
|
|
84
|
+
guard();
|
|
85
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || (before.mode & 0o111n) === 0n) failAgent('executable-unsafe');
|
|
86
|
+
const canonical = await realpath(path);
|
|
87
|
+
guard();
|
|
88
|
+
const after = await lstat(path, { bigint: true });
|
|
89
|
+
guard();
|
|
90
|
+
if (canonical !== path || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || !sameIdentity(before, after)) failAgent('executable-unsafe');
|
|
91
|
+
const current = Object.freeze({
|
|
92
|
+
...identity(after), size: after.size.toString(), mode: after.mode.toString(),
|
|
93
|
+
mtimeMs: after.mtimeMs.toString(), ctimeMs: after.ctimeMs.toString(),
|
|
94
|
+
});
|
|
95
|
+
if (expected && Reflect.ownKeys(current).some(key => expected[key] !== current[key])) failAgent('executable-unsafe');
|
|
96
|
+
return current;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof AgentContractError) throw error;
|
|
99
|
+
if (error?.code === 'ENOENT') failAgent('provider-unavailable');
|
|
100
|
+
failAgent('executable-unsafe');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sameSnapshot(left, right) {
|
|
105
|
+
return left && right && Reflect.ownKeys(left).length === Reflect.ownKeys(right).length
|
|
106
|
+
&& Reflect.ownKeys(left).every(key => left[key] === right[key]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function nativeHeader(header) {
|
|
110
|
+
const magic = header.subarray(0, 4).toString('hex');
|
|
111
|
+
if (process.platform === 'linux') return magic === '7f454c46';
|
|
112
|
+
return ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca'].includes(magic);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readPinnedHeader(path, expectedIdentity, guard = () => {}) {
|
|
116
|
+
let handle;
|
|
117
|
+
try {
|
|
118
|
+
handle = await open(path, 'r');
|
|
119
|
+
guard();
|
|
120
|
+
const metadata = await handle.stat({ bigint: true });
|
|
121
|
+
guard();
|
|
122
|
+
const current = Object.freeze({
|
|
123
|
+
...identity(metadata), size: metadata.size.toString(), mode: metadata.mode.toString(),
|
|
124
|
+
mtimeMs: metadata.mtimeMs.toString(), ctimeMs: metadata.ctimeMs.toString(),
|
|
125
|
+
});
|
|
126
|
+
if (!sameSnapshot(current, expectedIdentity)) failAgent('executable-unsafe');
|
|
127
|
+
const header = Buffer.alloc(512);
|
|
128
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
129
|
+
guard();
|
|
130
|
+
return header.subarray(0, bytesRead);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof AgentContractError) throw error;
|
|
133
|
+
failAgent('executable-unsafe');
|
|
134
|
+
} finally {
|
|
135
|
+
try { await handle?.close(); } catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function inspectNativeInterpreter(path, expectedIdentity, guard = () => {}) {
|
|
140
|
+
const interpreterIdentity = await verifyExecutable(path, expectedIdentity, guard);
|
|
141
|
+
const header = await readPinnedHeader(path, interpreterIdentity, guard);
|
|
142
|
+
await verifyExecutable(path, interpreterIdentity, guard);
|
|
143
|
+
guard();
|
|
144
|
+
if (!nativeHeader(header)) failAgent('executable-unsafe');
|
|
145
|
+
return interpreterIdentity;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function inspectLaunchTarget(executable, interpreter, expected, guard = () => {}) {
|
|
149
|
+
const executableIdentity = await verifyExecutable(executable, expected?.executableIdentity, guard);
|
|
150
|
+
const header = await readPinnedHeader(executable, executableIdentity, guard);
|
|
151
|
+
await verifyExecutable(executable, executableIdentity, guard);
|
|
152
|
+
guard();
|
|
153
|
+
let target;
|
|
154
|
+
if (nativeHeader(header)) {
|
|
155
|
+
if (interpreter !== undefined) failAgent('executable-unsafe');
|
|
156
|
+
target = Object.freeze({
|
|
157
|
+
kind: 'native', executableIdentity, interpreterIdentity: null,
|
|
158
|
+
spawnExecutable: executable, scriptPath: null,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
let firstLine;
|
|
162
|
+
try {
|
|
163
|
+
const newline = header.indexOf(0x0a);
|
|
164
|
+
if (newline < 3 || newline > 256) failAgent('executable-unsafe');
|
|
165
|
+
firstLine = new TextDecoder('utf-8', { fatal: true }).decode(header.subarray(0, newline));
|
|
166
|
+
} catch { failAgent('executable-unsafe'); }
|
|
167
|
+
const pinnedShebang = typeof interpreter === 'string' && firstLine === `#!${interpreter}`;
|
|
168
|
+
const npmNodeShebang = firstLine === '#!/usr/bin/env node' && typeof interpreter === 'string' && basename(interpreter) === 'node';
|
|
169
|
+
if (!pinnedShebang && !npmNodeShebang) failAgent('executable-unsafe');
|
|
170
|
+
const interpreterIdentity = await inspectNativeInterpreter(interpreter, expected?.interpreterIdentity, guard);
|
|
171
|
+
target = Object.freeze({
|
|
172
|
+
kind: 'script', executableIdentity, interpreterIdentity,
|
|
173
|
+
spawnExecutable: interpreter, scriptPath: executable,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (expected && (expected.kind !== target.kind || expected.spawnExecutable !== target.spawnExecutable
|
|
177
|
+
|| expected.scriptPath !== target.scriptPath)) failAgent('executable-unsafe');
|
|
178
|
+
return target;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function invocation(target, args) {
|
|
182
|
+
return target.kind === 'script'
|
|
183
|
+
? Object.freeze({ executable: target.spawnExecutable, args: Object.freeze([target.scriptPath, ...args]) })
|
|
184
|
+
: Object.freeze({ executable: target.spawnExecutable, args });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function verifyDirectory(path, expected, reason = 'cwd-unsafe', guard = () => {}) {
|
|
188
|
+
if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path.length > 1024) failAgent(reason);
|
|
189
|
+
try {
|
|
190
|
+
const before = await lstat(path, { bigint: true });
|
|
191
|
+
guard();
|
|
192
|
+
if (!before.isDirectory() || before.isSymbolicLink()) failAgent(reason);
|
|
193
|
+
const canonical = await realpath(path);
|
|
194
|
+
guard();
|
|
195
|
+
const after = await lstat(path, { bigint: true });
|
|
196
|
+
guard();
|
|
197
|
+
if (canonical !== path || !after.isDirectory() || after.isSymbolicLink() || !sameIdentity(before, after)) failAgent(reason);
|
|
198
|
+
const current = identity(after);
|
|
199
|
+
if (expected && (expected.dev !== current.dev || expected.ino !== current.ino)) failAgent(reason);
|
|
200
|
+
return current;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof AgentContractError) throw error;
|
|
203
|
+
failAgent(reason);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function within(parent, child) {
|
|
208
|
+
const path = relative(parent, child);
|
|
209
|
+
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function verifyCwd(worktree, value, guard = () => {}) {
|
|
213
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 500 || isAbsolute(value)
|
|
214
|
+
|| value.includes('//') || value.includes('\\') || value.includes(':') || /[\u0000-\u001f\u007f]/.test(value)) failAgent('cwd-unsafe');
|
|
215
|
+
const normalized = value.normalize('NFKC');
|
|
216
|
+
if (normalized !== value) failAgent('cwd-unsafe');
|
|
217
|
+
const parts = value === '.' ? [] : value.split('/');
|
|
218
|
+
if (parts.some(part => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ') || WINDOWS_RESERVED.test(part))) failAgent('cwd-unsafe');
|
|
219
|
+
let cursor = worktree;
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
cursor = join(cursor, part);
|
|
222
|
+
try {
|
|
223
|
+
const metadata = await lstat(cursor, { bigint: true });
|
|
224
|
+
guard();
|
|
225
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) failAgent('cwd-unsafe');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof AgentContractError) throw error;
|
|
228
|
+
failAgent('cwd-unsafe');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const candidate = resolve(worktree, ...parts);
|
|
232
|
+
if (!within(worktree, candidate)) failAgent('cwd-unsafe');
|
|
233
|
+
await verifyDirectory(candidate, undefined, 'cwd-unsafe', guard);
|
|
234
|
+
guard();
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function snapshotArgs(input, allowOptions) {
|
|
239
|
+
if (!Array.isArray(input)) failAgent('argument-invalid');
|
|
240
|
+
let length;
|
|
241
|
+
try { length = input.length; } catch { failAgent('argument-invalid'); }
|
|
242
|
+
if (!Number.isSafeInteger(length) || length > 128) failAgent('argument-invalid');
|
|
243
|
+
const output = [];
|
|
244
|
+
try {
|
|
245
|
+
for (let index = 0; index < length; index += 1) {
|
|
246
|
+
if (!Object.hasOwn(input, index)) failAgent('argument-invalid');
|
|
247
|
+
const value = input[index];
|
|
248
|
+
if (typeof value !== 'string' || value.length > 4096 || value.length === 0 || /[\u0000\r\n]/.test(value)
|
|
249
|
+
|| (!allowOptions && value.startsWith('-'))) failAgent('argument-invalid');
|
|
250
|
+
output.push(value);
|
|
251
|
+
}
|
|
252
|
+
} catch { failAgent('argument-invalid'); }
|
|
253
|
+
return Object.freeze(output);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function environment(input) {
|
|
257
|
+
if (input === undefined) input = Object.create(null);
|
|
258
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) failAgent('invalid-contract');
|
|
259
|
+
let keys;
|
|
260
|
+
try { keys = Reflect.ownKeys(input); } catch { failAgent('invalid-contract'); }
|
|
261
|
+
if (keys.length > 64 || keys.some(key => typeof key !== 'string')) failAgent('invalid-contract');
|
|
262
|
+
const result = Object.create(null);
|
|
263
|
+
try {
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
266
|
+
if (!descriptor?.enumerable) failAgent('invalid-contract');
|
|
267
|
+
const value = input[key];
|
|
268
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || typeof value !== 'string' || value.length > 32768 || value.includes('\0')) failAgent('invalid-contract');
|
|
269
|
+
if (SAFE_ENV.has(key) && !SENSITIVE_ENV.test(key)) result[key] = value;
|
|
270
|
+
}
|
|
271
|
+
} catch { failAgent('invalid-contract'); }
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
PATH: result.PATH ?? '/usr/bin:/bin',
|
|
274
|
+
LANG: result.LANG ?? 'C.UTF-8', LC_ALL: result.LC_ALL ?? 'C.UTF-8', TZ: result.TZ ?? 'UTC',
|
|
275
|
+
TERM: 'dumb', NO_COLOR: '1', CI: '1', GIT_TERMINAL_PROMPT: '0',
|
|
276
|
+
...(result.TMPDIR ? { TMPDIR: result.TMPDIR } : {}),
|
|
277
|
+
...(result.HOME ? { HOME: result.HOME } : {}),
|
|
278
|
+
...(result.USER !== undefined ? { USER: result.USER } : {}),
|
|
279
|
+
...(result.LOGNAME !== undefined ? { LOGNAME: result.LOGNAME } : {}),
|
|
280
|
+
...(result.SHELL !== undefined ? { SHELL: result.SHELL } : {}),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function strictDecode(buffer) {
|
|
285
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer); } catch { failAgent('output-invalid'); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function claudeMeasuredUsage(envelope) {
|
|
289
|
+
const budgetStop = envelope?.subtype === 'error_max_budget_usd';
|
|
290
|
+
if (envelope?.type !== 'result' || (budgetStop ? envelope.is_error !== true : envelope.subtype !== 'success' || envelope.is_error !== false)
|
|
291
|
+
|| !envelope.modelUsage || typeof envelope.modelUsage !== 'object' || Array.isArray(envelope.modelUsage)
|
|
292
|
+
|| typeof envelope.total_cost_usd !== 'number' || !Number.isFinite(envelope.total_cost_usd)
|
|
293
|
+
|| envelope.total_cost_usd < 0 || envelope.total_cost_usd > 100_000) failAgent('output-invalid');
|
|
294
|
+
const models = Object.values(envelope.modelUsage);
|
|
295
|
+
if (models.length === 0 || models.length > 64) failAgent('output-invalid');
|
|
296
|
+
let tokens = 0;
|
|
297
|
+
// modelUsage includes nested agents. The aggregate usage object covers only
|
|
298
|
+
// the main loop; adding both would count that loop twice. Cost is the native
|
|
299
|
+
// CLI's estimate, not a billing receipt or a model-written claim.
|
|
300
|
+
for (const model of models) {
|
|
301
|
+
if (!model || typeof model !== 'object' || Array.isArray(model)) failAgent('output-invalid');
|
|
302
|
+
for (const key of ['inputTokens', 'outputTokens', 'cacheCreationInputTokens', 'cacheReadInputTokens']) {
|
|
303
|
+
const count = model[key];
|
|
304
|
+
if (!Number.isSafeInteger(count) || count < 0) failAgent('output-invalid');
|
|
305
|
+
tokens += count;
|
|
306
|
+
if (!Number.isSafeInteger(tokens) || tokens > 10_000_000) failAgent('output-invalid');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return {tokens, costUsd: envelope.total_cost_usd};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseEnvelope(buffer, contract, resultFormat, requireBudgetStop = false) {
|
|
313
|
+
const source = strictDecode(buffer);
|
|
314
|
+
const document = source.trim();
|
|
315
|
+
if (!document || source.length - document.length > 16 || /[^\t\n\r ]/.test(source.slice(0, source.indexOf(document)))
|
|
316
|
+
|| /[^\t\n\r ]/.test(source.slice(source.indexOf(document) + document.length))) failAgent('output-invalid');
|
|
317
|
+
let parsed;
|
|
318
|
+
try { parsed = JSON.parse(document); } catch { failAgent('output-invalid'); }
|
|
319
|
+
const captured = immutableJson(parsed);
|
|
320
|
+
if (resultFormat === 'claude-json' && captured?.type === 'result' && captured.subtype === 'error_max_budget_usd') {
|
|
321
|
+
return immutableJson({version: 1, status: 'budget-exhausted',
|
|
322
|
+
output: {summary: 'Claude stopped at its configured cost limit.', evidence: []},
|
|
323
|
+
usage: claudeMeasuredUsage(captured)});
|
|
324
|
+
}
|
|
325
|
+
if (requireBudgetStop) failAgent('provider-unavailable');
|
|
326
|
+
let value = captured && typeof captured === 'object' && !Array.isArray(captured)
|
|
327
|
+
&& Object.hasOwn(captured, 'structured_output')
|
|
328
|
+
? (captured.type === 'result' && captured.subtype === 'success' && captured.structured_output
|
|
329
|
+
&& typeof captured.structured_output === 'object' && !Array.isArray(captured.structured_output)
|
|
330
|
+
? captured.structured_output
|
|
331
|
+
: failAgent('output-invalid'))
|
|
332
|
+
: captured;
|
|
333
|
+
const keys = Reflect.ownKeys(value);
|
|
334
|
+
if (keys.length !== 4 || !['version', 'status', 'output', 'usage'].every(key => Object.hasOwn(value, key))
|
|
335
|
+
|| value.version !== 1 || !['success', 'retry', 'failed', 'blocked', 'budget-exhausted'].includes(value.status)
|
|
336
|
+
|| !value.output || typeof value.output !== 'object' || Array.isArray(value.output)
|
|
337
|
+
|| !value.usage || typeof value.usage !== 'object' || Array.isArray(value.usage)) failAgent('output-invalid');
|
|
338
|
+
const outputKeys = Reflect.ownKeys(value.output);
|
|
339
|
+
const usageKeys = Reflect.ownKeys(value.usage);
|
|
340
|
+
if (outputKeys.length !== 2 || !['summary', 'evidence'].every(key => Object.hasOwn(value.output, key))
|
|
341
|
+
|| typeof value.output.summary !== 'string' || value.output.summary.length === 0 || value.output.summary.length > 16_384
|
|
342
|
+
|| containsSecretMaterial(value.output.summary)
|
|
343
|
+
|| !Array.isArray(value.output.evidence) || value.output.evidence.length > 128
|
|
344
|
+
|| value.output.evidence.some(item => typeof item !== 'string' || item.length === 0 || item.length > 500 || containsSecretMaterial(item))
|
|
345
|
+
|| usageKeys.length !== 2 || !['tokens', 'costUsd'].every(key => Object.hasOwn(value.usage, key))
|
|
346
|
+
|| !Number.isSafeInteger(value.usage.tokens) || value.usage.tokens < 0 || value.usage.tokens > 10_000_000
|
|
347
|
+
|| typeof value.usage.costUsd !== 'number' || !Number.isFinite(value.usage.costUsd) || value.usage.costUsd < 0 || value.usage.costUsd > 100_000) {
|
|
348
|
+
failAgent('output-invalid');
|
|
349
|
+
}
|
|
350
|
+
if (resultFormat === 'claude-json') {
|
|
351
|
+
const usage = claudeMeasuredUsage(captured);
|
|
352
|
+
const exhausted = usage.tokens > contract.budget.maxTokens || usage.costUsd > contract.budget.maxCostUsd;
|
|
353
|
+
value = immutableJson({...value, usage, status: exhausted ? 'budget-exhausted' : value.status});
|
|
354
|
+
}
|
|
355
|
+
if (value.status !== 'budget-exhausted'
|
|
356
|
+
&& (value.usage.tokens > contract.budget.maxTokens || value.usage.costUsd > contract.budget.maxCostUsd)) failAgent('output-invalid');
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function killTree(child, signal) {
|
|
361
|
+
try {
|
|
362
|
+
if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
|
|
363
|
+
else child.kill(signal);
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Sending SIGKILL is asynchronous: the direct child may close before its
|
|
368
|
+
// detached descendants finish exiting. Keep cleanup alive until the group is
|
|
369
|
+
// gone, or the bounded grace expires (a reparented zombie can retain the PGID).
|
|
370
|
+
async function settleTermination(child, graceMs) {
|
|
371
|
+
if (!child?.pid) return;
|
|
372
|
+
killTree(child, 'SIGKILL');
|
|
373
|
+
const deadline = performance.now() + graceMs;
|
|
374
|
+
while (performance.now() < deadline) {
|
|
375
|
+
try { process.kill(-child.pid, 0); }
|
|
376
|
+
catch (error) { if (error.code === 'ESRCH') return; }
|
|
377
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function createProcessRunner(input) {
|
|
382
|
+
assertSupportedClientPlatform();
|
|
383
|
+
const config = capture(input, CONFIG_KEYS, ['executable', 'worktree'], 'invalid-contract');
|
|
384
|
+
const timeoutMs = boundedInteger(config.timeoutMs, 30_000, 10 * 60_000);
|
|
385
|
+
const launchTimeoutMs = boundedInteger(config.launchTimeoutMs, 5_000, 30_000);
|
|
386
|
+
const termGraceMs = boundedInteger(config.termGraceMs, 250, 10_000);
|
|
387
|
+
const killGraceMs = boundedInteger(config.killGraceMs, 1_000, 30_000);
|
|
388
|
+
const maxOutputBytes = boundedInteger(config.maxOutputBytes, 256 * 1024, 10 * 1024 * 1024);
|
|
389
|
+
const maxInputBytes = boundedInteger(config.maxInputBytes, 128 * 1024, 1024 * 1024);
|
|
390
|
+
if (config.allowOptionArgs !== undefined && typeof config.allowOptionArgs !== 'boolean') failAgent('invalid-contract');
|
|
391
|
+
if (config.resultFormat !== undefined && config.resultFormat !== 'claude-json') failAgent('invalid-contract');
|
|
392
|
+
const executable = config.executable;
|
|
393
|
+
const interpreter = config.interpreter;
|
|
394
|
+
const worktree = config.worktree;
|
|
395
|
+
const boundSignal = captureSignal(config.signal);
|
|
396
|
+
let constructionCancelled = signalAborted(boundSignal);
|
|
397
|
+
if (constructionCancelled) failAgent('aborted');
|
|
398
|
+
const removeConstructionListener = watchSignal(boundSignal, () => { constructionCancelled = true; });
|
|
399
|
+
const constructionGuard = () => {
|
|
400
|
+
if (constructionCancelled || signalAborted(boundSignal)) failAgent('aborted');
|
|
401
|
+
};
|
|
402
|
+
let launchTarget;
|
|
403
|
+
let worktreeIdentity;
|
|
404
|
+
try {
|
|
405
|
+
constructionGuard();
|
|
406
|
+
launchTarget = await inspectLaunchTarget(executable, interpreter, undefined, constructionGuard);
|
|
407
|
+
constructionGuard();
|
|
408
|
+
worktreeIdentity = await verifyDirectory(worktree, undefined, 'cwd-unsafe', constructionGuard);
|
|
409
|
+
constructionGuard();
|
|
410
|
+
if (config.worktreeIdentity !== undefined) {
|
|
411
|
+
const supplied = capture(config.worktreeIdentity, new Set(['dev', 'ino']), ['dev', 'ino'], 'cwd-unsafe');
|
|
412
|
+
if (typeof supplied.dev !== 'string' || typeof supplied.ino !== 'string'
|
|
413
|
+
|| !/^[0-9]+$/.test(supplied.dev) || !/^[0-9]+$/.test(supplied.ino)
|
|
414
|
+
|| supplied.dev !== worktreeIdentity.dev || supplied.ino !== worktreeIdentity.ino) failAgent('cwd-unsafe');
|
|
415
|
+
}
|
|
416
|
+
} finally { removeConstructionListener(); }
|
|
417
|
+
const executionEnvironment = environment(config.environment);
|
|
418
|
+
|
|
419
|
+
async function probe(args, help = false) {
|
|
420
|
+
let cancelled = signalAborted(boundSignal);
|
|
421
|
+
if (cancelled) failAgent('aborted');
|
|
422
|
+
let terminateChild;
|
|
423
|
+
const removeListener = watchSignal(boundSignal, () => {
|
|
424
|
+
cancelled = true;
|
|
425
|
+
terminateChild?.('aborted');
|
|
426
|
+
});
|
|
427
|
+
const guard = () => { if (cancelled || signalAborted(boundSignal)) failAgent('aborted'); };
|
|
428
|
+
try {
|
|
429
|
+
await verifyDirectory(worktree, worktreeIdentity, 'cwd-unsafe', guard);
|
|
430
|
+
guard();
|
|
431
|
+
const currentTarget = await inspectLaunchTarget(executable, interpreter, launchTarget, guard);
|
|
432
|
+
guard();
|
|
433
|
+
const command = invocation(currentTarget, args);
|
|
434
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
435
|
+
let child;
|
|
436
|
+
let classification;
|
|
437
|
+
let cleanupDeadline;
|
|
438
|
+
let settled = false;
|
|
439
|
+
let timer;
|
|
440
|
+
let killTimer;
|
|
441
|
+
let hardTimer;
|
|
442
|
+
const chunks = [];
|
|
443
|
+
let bytes = 0;
|
|
444
|
+
const finish = (error, value) => {
|
|
445
|
+
if (settled) return;
|
|
446
|
+
settled = true;
|
|
447
|
+
clearTimeout(timer); clearTimeout(killTimer); clearTimeout(hardTimer);
|
|
448
|
+
if (error && classification && child) {
|
|
449
|
+
settleTermination(child, Math.max(0, cleanupDeadline - performance.now())).then(() => rejectPromise(error), () => rejectPromise(error));
|
|
450
|
+
} else if (error) rejectPromise(error); else resolvePromise(value);
|
|
451
|
+
};
|
|
452
|
+
terminateChild = reason => {
|
|
453
|
+
if (classification || !child) return;
|
|
454
|
+
classification = reason;
|
|
455
|
+
cleanupDeadline = performance.now() + termGraceMs + killGraceMs;
|
|
456
|
+
killTree(child, 'SIGTERM');
|
|
457
|
+
killTimer = setTimeout(() => killTree(child, 'SIGKILL'), termGraceMs);
|
|
458
|
+
killTimer.unref?.();
|
|
459
|
+
hardTimer = setTimeout(() => {
|
|
460
|
+
killTree(child, 'SIGKILL');
|
|
461
|
+
finish(new AgentContractError(reason));
|
|
462
|
+
}, termGraceMs + killGraceMs);
|
|
463
|
+
hardTimer.unref?.();
|
|
464
|
+
};
|
|
465
|
+
if (cancelled) { finish(new AgentContractError('aborted')); return; }
|
|
466
|
+
try {
|
|
467
|
+
child = spawn(command.executable, command.args, {
|
|
468
|
+
cwd: worktree, shell: false, windowsHide: true, detached: true,
|
|
469
|
+
env: executionEnvironment, stdio: ['ignore', 'pipe', 'pipe'],
|
|
470
|
+
});
|
|
471
|
+
} catch { finish(new AgentContractError('provider-unavailable')); return; }
|
|
472
|
+
if (cancelled) terminateChild('aborted');
|
|
473
|
+
const collect = (chunk, keep) => {
|
|
474
|
+
if (classification) return;
|
|
475
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
476
|
+
const available = Math.max(0, (help ? 65536 : 4096) - bytes);
|
|
477
|
+
if (keep && available > 0) chunks.push(buffer.subarray(0, available));
|
|
478
|
+
bytes += Math.min(buffer.byteLength, available);
|
|
479
|
+
if (buffer.byteLength > available) terminateChild('provider-unavailable');
|
|
480
|
+
};
|
|
481
|
+
child.stdout.on('data', chunk => collect(chunk, true));
|
|
482
|
+
child.stderr.on('data', chunk => collect(chunk, false));
|
|
483
|
+
child.once('error', () => {
|
|
484
|
+
if (classification) killTree(child, 'SIGKILL');
|
|
485
|
+
finish(new AgentContractError(classification ?? 'provider-unavailable'));
|
|
486
|
+
});
|
|
487
|
+
child.once('close', code => {
|
|
488
|
+
if (classification) {
|
|
489
|
+
// A detached adapter can exit before a descendant in its process
|
|
490
|
+
// group. Kill the group before clearing the escalation timer.
|
|
491
|
+
killTree(child, 'SIGKILL');
|
|
492
|
+
finish(new AgentContractError(classification));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (code !== 0) { finish(new AgentContractError('provider-unavailable')); return; }
|
|
496
|
+
try {
|
|
497
|
+
const value = strictDecode(Buffer.concat(chunks)).trim();
|
|
498
|
+
if (!value || (!help && (value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)))) failAgent('provider-unavailable');
|
|
499
|
+
finish(null, value);
|
|
500
|
+
} catch { finish(new AgentContractError('provider-unavailable')); }
|
|
501
|
+
});
|
|
502
|
+
timer = setTimeout(() => terminateChild('provider-unavailable'), Math.min(launchTimeoutMs, timeoutMs));
|
|
503
|
+
timer.unref?.();
|
|
504
|
+
});
|
|
505
|
+
} finally {
|
|
506
|
+
terminateChild = undefined;
|
|
507
|
+
removeListener();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function execute(inputRequest, protocol) {
|
|
512
|
+
const request = capture(inputRequest, REQUEST_KEYS, ['args', 'cwd', 'payload'], 'invalid-contract');
|
|
513
|
+
const args = snapshotArgs(request.args, config.allowOptionArgs === true);
|
|
514
|
+
if (typeof request.payload !== 'string' || Buffer.byteLength(request.payload, 'utf8') > maxInputBytes || request.payload.includes('\0')) failAgent('invalid-contract');
|
|
515
|
+
let contract;
|
|
516
|
+
try {
|
|
517
|
+
contract = protocol === 'launch'
|
|
518
|
+
? validateLaunchPayload(request.payload)
|
|
519
|
+
: validatePlanningPayload(request.payload);
|
|
520
|
+
} catch { failAgent('invalid-contract'); }
|
|
521
|
+
if (protocol !== 'launch' && request.protocolContext !== undefined) failAgent('invalid-contract');
|
|
522
|
+
let stdinPayload = protocol === 'launch'
|
|
523
|
+
? serializeProtocolLaunch(contract, request.protocolContext) : request.payload;
|
|
524
|
+
if (Buffer.byteLength(stdinPayload, 'utf8') > maxInputBytes) failAgent('invalid-contract');
|
|
525
|
+
if (contract.worktree.path !== worktree
|
|
526
|
+
|| contract.worktree.dev !== worktreeIdentity.dev || contract.worktree.ino !== worktreeIdentity.ino) failAgent('cwd-unsafe');
|
|
527
|
+
const requestSignal = captureSignal(request.signal);
|
|
528
|
+
if (boundSignal && requestSignal && boundSignal !== requestSignal) failAgent('invalid-contract');
|
|
529
|
+
const signal = requestSignal ?? boundSignal;
|
|
530
|
+
let cancelled = signalAborted(signal);
|
|
531
|
+
if (cancelled) failAgent('aborted');
|
|
532
|
+
let terminateChild;
|
|
533
|
+
const removeListener = watchSignal(signal, () => {
|
|
534
|
+
cancelled = true;
|
|
535
|
+
terminateChild?.('aborted');
|
|
536
|
+
});
|
|
537
|
+
const guard = () => { if (cancelled || signalAborted(signal)) failAgent('aborted'); };
|
|
538
|
+
try {
|
|
539
|
+
await verifyDirectory(worktree, worktreeIdentity, 'cwd-unsafe', guard);
|
|
540
|
+
guard();
|
|
541
|
+
const cwd = await verifyCwd(worktree, request.cwd, guard);
|
|
542
|
+
guard();
|
|
543
|
+
const currentTarget = await inspectLaunchTarget(executable, interpreter, launchTarget, guard);
|
|
544
|
+
guard();
|
|
545
|
+
if (protocol === 'launch') {
|
|
546
|
+
stdinPayload = serializeProtocolLaunch(contract, request.protocolContext);
|
|
547
|
+
if (Buffer.byteLength(stdinPayload, 'utf8') > maxInputBytes) failAgent('invalid-contract');
|
|
548
|
+
}
|
|
549
|
+
const command = invocation(currentTarget, args);
|
|
550
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
551
|
+
let child;
|
|
552
|
+
const stdout = [];
|
|
553
|
+
let totalBytes = 0;
|
|
554
|
+
let classification;
|
|
555
|
+
let cleanupDeadline;
|
|
556
|
+
let settled = false;
|
|
557
|
+
let runtimeTimer;
|
|
558
|
+
let launchTimer;
|
|
559
|
+
let termTimer;
|
|
560
|
+
let hardTimer;
|
|
561
|
+
const finish = (error, value) => {
|
|
562
|
+
if (settled) return;
|
|
563
|
+
settled = true;
|
|
564
|
+
clearTimeout(runtimeTimer); clearTimeout(launchTimer); clearTimeout(termTimer); clearTimeout(hardTimer);
|
|
565
|
+
if (error && classification && child) {
|
|
566
|
+
settleTermination(child, Math.max(0, cleanupDeadline - performance.now())).then(() => rejectPromise(error), () => rejectPromise(error));
|
|
567
|
+
} else if (error) rejectPromise(error); else resolvePromise(value);
|
|
568
|
+
};
|
|
569
|
+
terminateChild = reason => {
|
|
570
|
+
if (classification || !child) return;
|
|
571
|
+
classification = reason;
|
|
572
|
+
cleanupDeadline = performance.now() + termGraceMs + killGraceMs;
|
|
573
|
+
killTree(child, 'SIGTERM');
|
|
574
|
+
termTimer = setTimeout(() => killTree(child, 'SIGKILL'), termGraceMs);
|
|
575
|
+
termTimer.unref?.();
|
|
576
|
+
hardTimer = setTimeout(() => {
|
|
577
|
+
killTree(child, 'SIGKILL');
|
|
578
|
+
finish(new AgentContractError(reason));
|
|
579
|
+
}, termGraceMs + killGraceMs);
|
|
580
|
+
hardTimer.unref?.();
|
|
581
|
+
};
|
|
582
|
+
if (cancelled) { finish(new AgentContractError('aborted')); return; }
|
|
583
|
+
try {
|
|
584
|
+
child = spawn(command.executable, command.args, {
|
|
585
|
+
cwd, shell: false, windowsHide: true, detached: true,
|
|
586
|
+
env: executionEnvironment, stdio: ['pipe', 'pipe', 'pipe'],
|
|
587
|
+
});
|
|
588
|
+
} catch { finish(new AgentContractError('spawn-failed')); return; }
|
|
589
|
+
const collect = (target, chunk) => {
|
|
590
|
+
if (classification) return;
|
|
591
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
592
|
+
const available = Math.max(0, maxOutputBytes - totalBytes);
|
|
593
|
+
if (target && available > 0) target.push(buffer.subarray(0, available));
|
|
594
|
+
totalBytes += Math.min(buffer.byteLength, available);
|
|
595
|
+
if (buffer.byteLength > available) terminateChild('output-overflow');
|
|
596
|
+
};
|
|
597
|
+
child.stdout.on('data', chunk => collect(stdout, chunk));
|
|
598
|
+
child.stderr.on('data', chunk => collect(null, chunk));
|
|
599
|
+
// Once spawn() has returned a child, a closed provider stdin is an
|
|
600
|
+
// execution/availability failure. Classifying EPIPE as a spawn failure
|
|
601
|
+
// makes the result depend on whether stdin or close wins the event race.
|
|
602
|
+
child.stdin.on('error', () => terminateChild('provider-unavailable'));
|
|
603
|
+
child.once('error', () => {
|
|
604
|
+
if (classification) killTree(child, 'SIGKILL');
|
|
605
|
+
finish(new AgentContractError(classification ?? 'spawn-failed'));
|
|
606
|
+
});
|
|
607
|
+
child.once('spawn', () => clearTimeout(launchTimer));
|
|
608
|
+
child.once('close', code => {
|
|
609
|
+
if (classification) {
|
|
610
|
+
// The direct adapter may be gone while its descendants remain.
|
|
611
|
+
killTree(child, 'SIGKILL');
|
|
612
|
+
finish(new AgentContractError(classification));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// The child emitted a close event, so it did spawn successfully. A
|
|
616
|
+
// non-zero exit is a provider execution failure, not a spawn failure;
|
|
617
|
+
// classify it as unavailable so the runtime can apply its bounded
|
|
618
|
+
// transient-provider retry policy.
|
|
619
|
+
if (code !== 0 && !(protocol === 'launch' && config.resultFormat === 'claude-json')) {
|
|
620
|
+
finish(new AgentContractError('provider-unavailable')); return;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const bytes = Buffer.concat(stdout);
|
|
624
|
+
// A native cost-cap result can accompany a nonzero process exit.
|
|
625
|
+
// Only that validated terminal envelope preserves its accounting;
|
|
626
|
+
// any other nonzero exit remains a failed provider execution.
|
|
627
|
+
finish(null, protocol === 'launch' ? parseEnvelope(bytes, contract, config.resultFormat, code !== 0) : parsePlanningResult(bytes));
|
|
628
|
+
} catch (error) {
|
|
629
|
+
finish(error instanceof AgentContractError ? error : new AgentContractError('output-invalid'));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
const runtimeLimit = protocol === 'launch' ? Math.min(timeoutMs, contract.budget.maxRuntimeMs) : timeoutMs;
|
|
633
|
+
runtimeTimer = setTimeout(() => terminateChild('timeout'), runtimeLimit);
|
|
634
|
+
runtimeTimer.unref?.();
|
|
635
|
+
launchTimer = setTimeout(() => terminateChild('launch-timeout'), Math.min(launchTimeoutMs, runtimeLimit));
|
|
636
|
+
launchTimer.unref?.();
|
|
637
|
+
if (cancelled) { terminateChild('aborted'); return; }
|
|
638
|
+
try { child.stdin.end(stdinPayload, 'utf8'); } catch { terminateChild('provider-unavailable'); }
|
|
639
|
+
});
|
|
640
|
+
} finally {
|
|
641
|
+
terminateChild = undefined;
|
|
642
|
+
removeListener();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function run(inputRequest) { return execute(inputRequest, 'launch'); }
|
|
647
|
+
async function runPlanning(inputRequest) { return execute(inputRequest, 'planning'); }
|
|
648
|
+
|
|
649
|
+
return Object.freeze({
|
|
650
|
+
probeVersion: () => probe(['--version']),
|
|
651
|
+
probeHelp: provider => {
|
|
652
|
+
if (!['claude', 'codex'].includes(provider)) failAgent('invalid-contract');
|
|
653
|
+
return probe(provider === 'codex' ? ['exec', '--help'] : ['--help'], true);
|
|
654
|
+
},
|
|
655
|
+
run, runPlanning,
|
|
656
|
+
});
|
|
657
|
+
}
|