@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,803 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { verifyApproval } from './approvals.js';
|
|
8
|
+
import { evaluateAuthority } from './authority.js';
|
|
9
|
+
import { redactSecrets } from '../state/redact.js';
|
|
10
|
+
|
|
11
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
12
|
+
const ACTION = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
13
|
+
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
14
|
+
const SENSITIVE_ENVIRONMENT_KEY = /(?:^|_)(?:API_KEY|ACCESS_KEY|PRIVATE_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH(?:ORIZATION)?)(?:_|$)/i;
|
|
15
|
+
const SHELL_METACHARACTER = /[;&|`$<>*?!(){}[\]~\r\n\u0000]/;
|
|
16
|
+
const UNSAFE_EXECUTABLE = /^(?:sh|bash|dash|mksh|yash|zsh|fish|nu|elvish|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh(?:\.exe)?|node(?:\.exe)?|python\d*(?:\.exe)?|ruby(?:\.exe)?|perl(?:\.exe)?|env|sudo)$/i;
|
|
17
|
+
const SHELL_EXECUTABLE = /^(?:sh|bash|dash|mksh|yash|zsh|fish|nu|elvish|cmd|powershell|pwsh|env|sudo)(?:\.exe|\.cmd)?$/i;
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
|
+
const MAX_TIMEOUT_MS = 10 * 60_000;
|
|
20
|
+
const DEFAULT_OUTPUT_BYTES = 256 * 1024;
|
|
21
|
+
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
const REDACTION_LOOKAHEAD_BYTES = 4 * 1024;
|
|
23
|
+
const KILL_GRACE_MS = 100;
|
|
24
|
+
const BOOTSTRAP_KILL_GRACE_MS = 300;
|
|
25
|
+
const HARD_STOP_MS = 1_000;
|
|
26
|
+
const BOOTSTRAP_VERSION = 1;
|
|
27
|
+
const BOOTSTRAP_FLAG = '--agilno-command-bootstrap-v1';
|
|
28
|
+
const BOOTSTRAP_PATH = fileURLToPath(new URL('./command-bootstrap.js', import.meta.url));
|
|
29
|
+
const RESULT_FRAMING = /[\s\p{P}\p{S}\p{Cf}\p{Cc}]/u;
|
|
30
|
+
const MAX_RESULT_FRAMING = 256;
|
|
31
|
+
const preparedData = new WeakMap();
|
|
32
|
+
|
|
33
|
+
export class CommandPolicyError extends Error {
|
|
34
|
+
constructor(reason = 'invalid-command') {
|
|
35
|
+
const messages = {
|
|
36
|
+
'command-not-allowlisted': 'Command is not allowlisted.',
|
|
37
|
+
'shell-metacharacter': 'Command argument contains a shell metacharacter.',
|
|
38
|
+
'unsafe-executable': 'Command has an unsafe executable.',
|
|
39
|
+
'worktree-boundary': 'Command cwd must remain inside the assigned worktree.',
|
|
40
|
+
'cwd-symlink': 'Command cwd contains a symlink.',
|
|
41
|
+
'authority-denied': 'Command authority denied.',
|
|
42
|
+
'approval-required': 'Command approval required.',
|
|
43
|
+
};
|
|
44
|
+
super(messages[reason] ?? 'Command policy input is invalid.');
|
|
45
|
+
this.name = 'CommandPolicyError';
|
|
46
|
+
this.code = 'ERR_COMMAND_POLICY';
|
|
47
|
+
this.safeMessage = this.message;
|
|
48
|
+
this.details = Object.freeze({ reason });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fail(reason) { throw new CommandPolicyError(reason); }
|
|
53
|
+
|
|
54
|
+
function capture(value, allowed, required, reason) {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(reason);
|
|
56
|
+
let keys;
|
|
57
|
+
try { keys = Reflect.ownKeys(value); } catch { fail(reason); }
|
|
58
|
+
if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail(reason);
|
|
59
|
+
const result = Object.create(null);
|
|
60
|
+
try {
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
63
|
+
if (!descriptor?.enumerable) fail(reason);
|
|
64
|
+
result[key] = value[key];
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof CommandPolicyError) throw error;
|
|
68
|
+
fail(reason);
|
|
69
|
+
}
|
|
70
|
+
if (required.some(key => !Object.hasOwn(result, key))) fail(reason);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function captureArray(value, limit, itemLimit, reason) {
|
|
75
|
+
if (!Array.isArray(value)) fail(reason);
|
|
76
|
+
const result = [];
|
|
77
|
+
try {
|
|
78
|
+
const length = value.length;
|
|
79
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > limit) fail(reason);
|
|
80
|
+
for (let index = 0; index < length; index += 1) {
|
|
81
|
+
if (!Object.hasOwn(value, index)) fail(reason);
|
|
82
|
+
const item = value[index];
|
|
83
|
+
if (typeof item !== 'string' || item.length > itemLimit || item.includes('\0')) fail(reason);
|
|
84
|
+
result.push(item);
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof CommandPolicyError) throw error;
|
|
88
|
+
fail(reason);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function captureDictionary(value, limit, reason) {
|
|
94
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(reason);
|
|
95
|
+
let keys;
|
|
96
|
+
try { keys = Reflect.ownKeys(value); } catch { fail(reason); }
|
|
97
|
+
if (keys.length > limit || keys.some(key => typeof key !== 'string')) fail(reason);
|
|
98
|
+
const result = Object.create(null);
|
|
99
|
+
try {
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
102
|
+
if (!descriptor?.enumerable) fail(reason);
|
|
103
|
+
result[key] = value[key];
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof CommandPolicyError) throw error;
|
|
107
|
+
fail(reason);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function captureEnvironment(value) {
|
|
113
|
+
if (value === undefined) return Object.freeze(Object.create(null));
|
|
114
|
+
const environment = captureDictionary(value, 256, 'invalid-environment');
|
|
115
|
+
for (const [key, child] of Object.entries(environment)) {
|
|
116
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || typeof child !== 'string' || child.includes('\0') || child.length > 32_768) fail('invalid-environment');
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze(environment);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function boundedInteger(value, fallback, maximum, reason) {
|
|
122
|
+
const result = value ?? fallback;
|
|
123
|
+
if (!Number.isSafeInteger(result) || result <= 0 || result > maximum) fail(reason);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function validId(value) {
|
|
128
|
+
return typeof value === 'string' && value.length <= 64 && ID.test(value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validAction(value) {
|
|
132
|
+
return typeof value === 'string' && value.length <= 100 && ACTION.test(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function snapshotInputs(contractInput, requestInput) {
|
|
136
|
+
const contract = capture(contractInput, new Set([
|
|
137
|
+
'worktree', 'authority', 'commands', 'timeoutMs', 'maxOutputBytes', 'maxStreamOutputBytes', 'environment',
|
|
138
|
+
]), ['worktree', 'authority', 'commands'], 'invalid-contract');
|
|
139
|
+
const request = capture(requestInput, new Set([
|
|
140
|
+
'actorId', 'commandId', 'cwd', 'args', 'approval', 'approvalRegistry', 'nowMs',
|
|
141
|
+
]), ['actorId', 'commandId', 'cwd'], 'invalid-request');
|
|
142
|
+
const commands = captureDictionary(contract.commands, 64, 'invalid-commands');
|
|
143
|
+
if (!validId(request.commandId)) fail('invalid-command-id');
|
|
144
|
+
const configured = commands[request.commandId];
|
|
145
|
+
if (configured === undefined) fail('command-not-allowlisted');
|
|
146
|
+
const command = capture(configured, new Set([
|
|
147
|
+
'executable', 'args', 'action', 'execution', 'allowExtraArgs', 'elevated', 'approvalPolicyId', 'approverId',
|
|
148
|
+
]), ['executable', 'args', 'action'], 'invalid-command-entry');
|
|
149
|
+
if (command.execution !== undefined && command.execution !== 'argv') fail('invalid-command-entry');
|
|
150
|
+
if (command.execution === 'argv' && command.allowExtraArgs === true) fail('unexpected-command-args');
|
|
151
|
+
const configuredArgs = captureArray(command.args, 256, 4_096, 'invalid-command-args');
|
|
152
|
+
const extraArgs = request.args === undefined ? [] : captureArray(request.args, 128, 4_096, 'invalid-command-args');
|
|
153
|
+
const environment = captureEnvironment(contract.environment);
|
|
154
|
+
|
|
155
|
+
if (typeof contract.worktree !== 'string' || !isAbsolute(contract.worktree) || contract.worktree.length > 1_024) fail('invalid-worktree');
|
|
156
|
+
if (!validId(request.actorId)) fail('invalid-actor');
|
|
157
|
+
if (typeof command.action !== 'string' || command.action.length > 100 || !ACTION.test(command.action)) fail('invalid-command-action');
|
|
158
|
+
if (command.action !== `command.${request.commandId}` && command.action !== 'dependency.install') fail('invalid-command-action');
|
|
159
|
+
if (command.allowExtraArgs !== undefined && typeof command.allowExtraArgs !== 'boolean') fail('invalid-command-entry');
|
|
160
|
+
if (extraArgs.length > 0 && command.allowExtraArgs !== true) fail('unexpected-command-args');
|
|
161
|
+
const args = [...configuredArgs, ...extraArgs];
|
|
162
|
+
if (command.execution === 'argv') {
|
|
163
|
+
if (args.some(argument => /[\u0000-\u001f\u007f-\u009f]/.test(argument))) fail('invalid-command-args');
|
|
164
|
+
} else if (args.some(argument => SHELL_METACHARACTER.test(argument))) fail('shell-metacharacter');
|
|
165
|
+
const elevated = command.elevated ?? false;
|
|
166
|
+
if (typeof elevated !== 'boolean') fail('invalid-command-entry');
|
|
167
|
+
if (command.action === 'dependency.install' && !elevated) fail('approval-required');
|
|
168
|
+
if (elevated) {
|
|
169
|
+
if (!validAction(command.approvalPolicyId) || !validId(command.approverId)) fail('invalid-command-entry');
|
|
170
|
+
} else if (command.approvalPolicyId !== undefined || command.approverId !== undefined) fail('invalid-command-entry');
|
|
171
|
+
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
worktree: contract.worktree,
|
|
174
|
+
authority: contract.authority,
|
|
175
|
+
executable: command.executable,
|
|
176
|
+
execution: command.execution,
|
|
177
|
+
args: Object.freeze(args),
|
|
178
|
+
action: command.action,
|
|
179
|
+
elevated,
|
|
180
|
+
approvalPolicyId: command.approvalPolicyId,
|
|
181
|
+
approverId: command.approverId,
|
|
182
|
+
environment,
|
|
183
|
+
timeoutMs: boundedInteger(contract.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, 'invalid-timeout'),
|
|
184
|
+
maxOutputBytes: boundedInteger(contract.maxOutputBytes, DEFAULT_OUTPUT_BYTES, MAX_OUTPUT_BYTES, 'invalid-output-limit'),
|
|
185
|
+
maxStreamOutputBytes: boundedInteger(contract.maxStreamOutputBytes, contract.maxOutputBytes ?? DEFAULT_OUTPUT_BYTES, MAX_OUTPUT_BYTES, 'invalid-output-limit'),
|
|
186
|
+
actorId: request.actorId,
|
|
187
|
+
commandId: request.commandId,
|
|
188
|
+
cwd: request.cwd,
|
|
189
|
+
approval: request.approval,
|
|
190
|
+
approvalRegistry: request.approvalRegistry,
|
|
191
|
+
nowMs: request.nowMs,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isWithin(parent, candidate) {
|
|
196
|
+
const path = relative(parent, candidate);
|
|
197
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sameIdentity(left, right) {
|
|
201
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function identity(metadata) {
|
|
205
|
+
return Object.freeze({ dev: metadata.dev.toString(), ino: metadata.ino.toString() });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function verifiedDirectory(path, reason) {
|
|
209
|
+
const before = await lstat(path, { bigint: true });
|
|
210
|
+
if (!before.isDirectory() || before.isSymbolicLink()) fail(reason);
|
|
211
|
+
const canonical = await realpath(path);
|
|
212
|
+
const after = await lstat(path, { bigint: true });
|
|
213
|
+
if (canonical !== path || !after.isDirectory() || after.isSymbolicLink() || !sameIdentity(before, after)) fail(reason);
|
|
214
|
+
return Object.freeze({ path, identity: identity(after) });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function safeCwdPath(worktree, cwd) {
|
|
218
|
+
if (typeof cwd !== 'string' || cwd.length === 0 || cwd.length > 500 || /[\u0000\r\n:\\]/.test(cwd)) fail('invalid-cwd');
|
|
219
|
+
if (cwd === '.') return worktree;
|
|
220
|
+
if (isAbsolute(cwd) || cwd.includes('//') || cwd.endsWith('/')) fail('worktree-boundary');
|
|
221
|
+
const normalized = cwd.normalize('NFKC');
|
|
222
|
+
if (normalized !== cwd && /[/\\:]/.test(normalized)) fail('invalid-cwd');
|
|
223
|
+
if (/[\u0000\r\n:\\]/.test(normalized) || normalized.includes('//')) fail('invalid-cwd');
|
|
224
|
+
const parts = normalized.split('/');
|
|
225
|
+
if (parts.includes('..')) fail('worktree-boundary');
|
|
226
|
+
if (parts.some(part => (
|
|
227
|
+
part === '' || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
|
|
228
|
+
|| WINDOWS_RESERVED.test(part)
|
|
229
|
+
))) fail('invalid-cwd');
|
|
230
|
+
const candidate = resolve(worktree, normalized);
|
|
231
|
+
if (!isWithin(worktree, candidate)) fail('worktree-boundary');
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function verifyCwd(worktree, cwdInput) {
|
|
236
|
+
const candidate = safeCwdPath(worktree, cwdInput);
|
|
237
|
+
const relativePath = relative(worktree, candidate);
|
|
238
|
+
let cursor = worktree;
|
|
239
|
+
if (relativePath) {
|
|
240
|
+
for (const component of relativePath.split(sep)) {
|
|
241
|
+
cursor = join(cursor, component);
|
|
242
|
+
const metadata = await lstat(cursor, { bigint: true });
|
|
243
|
+
if (metadata.isSymbolicLink()) fail('cwd-symlink');
|
|
244
|
+
if (!metadata.isDirectory()) fail('invalid-cwd');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const verified = await verifiedDirectory(candidate, 'cwd-symlink');
|
|
248
|
+
if (!isWithin(worktree, verified.path)) fail('worktree-boundary');
|
|
249
|
+
return verified;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function verifyCommandExecutable(executable, options = {}) {
|
|
253
|
+
if (options.execution !== undefined && options.execution !== 'argv') fail('unsafe-executable');
|
|
254
|
+
const direct = options.execution === 'argv';
|
|
255
|
+
if (typeof executable !== 'string' || executable.length === 0 || executable.length > 1_024 || !isAbsolute(executable)) fail('unsafe-executable');
|
|
256
|
+
if (direct ? /[\u0000-\u001f\u007f-\u009f]/.test(executable) || SHELL_EXECUTABLE.test(basename(executable))
|
|
257
|
+
: SHELL_METACHARACTER.test(executable) || UNSAFE_EXECUTABLE.test(basename(executable))) fail('unsafe-executable');
|
|
258
|
+
const before = await lstat(executable, { bigint: true });
|
|
259
|
+
if (direct) {
|
|
260
|
+
if (!before.isFile() && !before.isSymbolicLink()) fail('unsafe-executable');
|
|
261
|
+
const parent = await verifiedDirectory(dirname(executable), 'unsafe-executable');
|
|
262
|
+
const canonical = await realpath(executable);
|
|
263
|
+
if (canonical.length > 1_024 || /[\u0000-\u001f\u007f-\u009f]/.test(canonical)
|
|
264
|
+
|| SHELL_EXECUTABLE.test(basename(canonical))) fail('unsafe-executable');
|
|
265
|
+
const target = await lstat(canonical, { bigint: true });
|
|
266
|
+
if (!target.isFile() || target.isSymbolicLink() || (target.mode & 0o111n) === 0n) fail('unsafe-executable');
|
|
267
|
+
const invocation = Object.freeze({
|
|
268
|
+
target: canonical, parent: parent.path, identity: identity(before), parentIdentity: parent.identity,
|
|
269
|
+
});
|
|
270
|
+
if (!await invocationStillValid(executable, invocation, identity(target))) fail('unsafe-executable');
|
|
271
|
+
return Object.freeze({ path: executable, identity: identity(target), invocation });
|
|
272
|
+
}
|
|
273
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || (before.mode & 0o111n) === 0n) fail('unsafe-executable');
|
|
274
|
+
const canonical = await realpath(executable);
|
|
275
|
+
const after = await lstat(executable, { bigint: true });
|
|
276
|
+
if (canonical !== executable || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || !sameIdentity(before, after)) fail('unsafe-executable');
|
|
277
|
+
return Object.freeze({ path: executable, identity: identity(after) });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function sanitizedFailure(error) {
|
|
281
|
+
return error instanceof CommandPolicyError ? error : new CommandPolicyError('invalid-command');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function approvalForElevated(input, worktree) {
|
|
285
|
+
if (!input.approval || !input.approvalRegistry || input.nowMs === undefined) fail('approval-required');
|
|
286
|
+
const verified = verifyApproval(input.approval, {
|
|
287
|
+
subjectId: input.actorId,
|
|
288
|
+
action: input.action,
|
|
289
|
+
resource: `command:${input.commandId}:${worktree}`,
|
|
290
|
+
policyId: input.approvalPolicyId,
|
|
291
|
+
}, {
|
|
292
|
+
registry: input.approvalRegistry,
|
|
293
|
+
expectedApproverId: input.approverId,
|
|
294
|
+
requireHumanApprover: true,
|
|
295
|
+
requireSingleUse: true,
|
|
296
|
+
nowMs: input.nowMs,
|
|
297
|
+
});
|
|
298
|
+
if (!verified.valid) fail('approval-required');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function prepareCommand(contractInput, requestInput) {
|
|
302
|
+
try {
|
|
303
|
+
const input = snapshotInputs(contractInput, requestInput);
|
|
304
|
+
const worktree = resolve(input.worktree);
|
|
305
|
+
const verifiedWorktree = await verifiedDirectory(worktree, 'invalid-worktree');
|
|
306
|
+
const executable = await verifyCommandExecutable(input.executable, { execution: input.execution });
|
|
307
|
+
const cwd = await verifyCwd(worktree, input.cwd);
|
|
308
|
+
if (input.maxStreamOutputBytes > input.maxOutputBytes) fail('invalid-output-limit');
|
|
309
|
+
const authorityDecision = evaluateAuthority(input.authority, {
|
|
310
|
+
actorId: input.actorId,
|
|
311
|
+
action: input.action,
|
|
312
|
+
commandId: input.commandId,
|
|
313
|
+
resource: input.commandId,
|
|
314
|
+
});
|
|
315
|
+
if (authorityDecision.decision === 'deny') fail('authority-denied');
|
|
316
|
+
if (authorityDecision.decision === 'approval-required' && !input.elevated) fail('approval-required');
|
|
317
|
+
if (input.elevated) approvalForElevated(input, worktree);
|
|
318
|
+
|
|
319
|
+
const prepared = Object.freeze({
|
|
320
|
+
executable: executable.path,
|
|
321
|
+
args: input.args,
|
|
322
|
+
cwd: cwd.path,
|
|
323
|
+
shell: false,
|
|
324
|
+
timeoutMs: input.timeoutMs,
|
|
325
|
+
maxOutputBytes: input.maxOutputBytes,
|
|
326
|
+
maxStreamOutputBytes: input.maxStreamOutputBytes,
|
|
327
|
+
commandId: input.commandId,
|
|
328
|
+
});
|
|
329
|
+
preparedData.set(prepared, Object.freeze({
|
|
330
|
+
worktree: verifiedWorktree.path,
|
|
331
|
+
environment: input.environment,
|
|
332
|
+
execution: input.execution,
|
|
333
|
+
invocation: executable.invocation,
|
|
334
|
+
identities: Object.freeze({
|
|
335
|
+
worktree: verifiedWorktree.identity,
|
|
336
|
+
cwd: cwd.identity,
|
|
337
|
+
executable: executable.identity,
|
|
338
|
+
}),
|
|
339
|
+
}));
|
|
340
|
+
return prepared;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw sanitizedFailure(error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function captureAbortSignal(signal) {
|
|
347
|
+
if (signal === undefined) return Object.freeze({ aborted: () => false, onAbort: () => {}, cleanup: () => {} });
|
|
348
|
+
if (!signal || typeof signal !== 'object') fail('invalid-abort-signal');
|
|
349
|
+
let add;
|
|
350
|
+
let remove;
|
|
351
|
+
try {
|
|
352
|
+
add = signal.addEventListener;
|
|
353
|
+
remove = signal.removeEventListener;
|
|
354
|
+
} catch { fail('invalid-abort-signal'); }
|
|
355
|
+
if (typeof add !== 'function' || typeof remove !== 'function') fail('invalid-abort-signal');
|
|
356
|
+
let aborted = false;
|
|
357
|
+
const callbacks = new Set();
|
|
358
|
+
const listener = () => {
|
|
359
|
+
aborted = true;
|
|
360
|
+
for (const callback of callbacks) callback();
|
|
361
|
+
};
|
|
362
|
+
try {
|
|
363
|
+
add.call(signal, 'abort', listener, { once: true });
|
|
364
|
+
aborted = signal.aborted;
|
|
365
|
+
} catch {
|
|
366
|
+
try { remove.call(signal, 'abort', listener); } catch {}
|
|
367
|
+
fail('invalid-abort-signal');
|
|
368
|
+
}
|
|
369
|
+
if (typeof aborted !== 'boolean') fail('invalid-abort-signal');
|
|
370
|
+
let cleaned = false;
|
|
371
|
+
return Object.freeze({
|
|
372
|
+
aborted: () => aborted,
|
|
373
|
+
onAbort(callback) {
|
|
374
|
+
if (aborted) callback();
|
|
375
|
+
else callbacks.add(callback);
|
|
376
|
+
},
|
|
377
|
+
cleanup() {
|
|
378
|
+
if (cleaned) return;
|
|
379
|
+
cleaned = true;
|
|
380
|
+
callbacks.clear();
|
|
381
|
+
try { remove.call(signal, 'abort', listener); } catch {}
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function sameVerifiedPath(path, expected, kind) {
|
|
387
|
+
try {
|
|
388
|
+
const metadata = await lstat(path, { bigint: true });
|
|
389
|
+
const canonical = await realpath(path);
|
|
390
|
+
const matchesKind = kind === 'file' ? metadata.isFile() : metadata.isDirectory();
|
|
391
|
+
return matchesKind && !metadata.isSymbolicLink() && canonical === path
|
|
392
|
+
&& metadata.dev.toString() === expected.dev && metadata.ino.toString() === expected.ino;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function invocationStillValid(path, invocation, targetIdentity) {
|
|
399
|
+
try {
|
|
400
|
+
const [entry, targetPath, parentValid, targetValid] = await Promise.all([
|
|
401
|
+
lstat(path, { bigint: true }), realpath(path),
|
|
402
|
+
sameVerifiedPath(invocation.parent, invocation.parentIdentity, 'directory'),
|
|
403
|
+
sameVerifiedPath(invocation.target, targetIdentity, 'file'),
|
|
404
|
+
]);
|
|
405
|
+
return (entry.isFile() || entry.isSymbolicLink()) && targetPath === invocation.target
|
|
406
|
+
&& dirname(path) === invocation.parent && parentValid && targetValid
|
|
407
|
+
&& entry.dev.toString() === invocation.identity.dev && entry.ino.toString() === invocation.identity.ino;
|
|
408
|
+
} catch { return false; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function anchorsStillValid(prepared, data) {
|
|
412
|
+
const [worktree, cwd, executable] = await Promise.all([
|
|
413
|
+
sameVerifiedPath(data.worktree, data.identities.worktree, 'directory'),
|
|
414
|
+
sameVerifiedPath(prepared.cwd, data.identities.cwd, 'directory'),
|
|
415
|
+
data.execution === 'argv'
|
|
416
|
+
? invocationStillValid(prepared.executable, data.invocation, data.identities.executable)
|
|
417
|
+
: sameVerifiedPath(prepared.executable, data.identities.executable, 'file'),
|
|
418
|
+
]);
|
|
419
|
+
return worktree && cwd && executable && isWithin(data.worktree, prepared.cwd);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function exactMessage(message, keys) {
|
|
423
|
+
return message && typeof message === 'object' && !Array.isArray(message)
|
|
424
|
+
&& Reflect.ownKeys(message).length === keys.length && keys.every(key => Object.hasOwn(message, key));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function rawCaptureLimit(prepared, environment) {
|
|
428
|
+
let environmentBytes = 0;
|
|
429
|
+
for (const value of Object.values(environment)) environmentBytes = Math.max(environmentBytes, Buffer.byteLength(value));
|
|
430
|
+
return prepared.maxOutputBytes + (2 * Math.max(REDACTION_LOOKAHEAD_BYTES, environmentBytes));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function executeAnchored(prepared, data, abortHandle) {
|
|
434
|
+
return new Promise(resolvePromise => {
|
|
435
|
+
const nonce = randomBytes(16).toString('hex');
|
|
436
|
+
const rawLimit = rawCaptureLimit(prepared, data.environment);
|
|
437
|
+
const chunks = { stdout: [], stderr: [] };
|
|
438
|
+
let rawBytes = 0;
|
|
439
|
+
let rawOverflow = false;
|
|
440
|
+
let overflowStream;
|
|
441
|
+
let child;
|
|
442
|
+
let resultMessage;
|
|
443
|
+
let timedOut = false;
|
|
444
|
+
let aborted = abortHandle.aborted();
|
|
445
|
+
let bootstrapError = false;
|
|
446
|
+
let launched = false;
|
|
447
|
+
let settled = false;
|
|
448
|
+
let stopping = false;
|
|
449
|
+
let killTimer;
|
|
450
|
+
let termTimer;
|
|
451
|
+
let timeoutTimer;
|
|
452
|
+
let hardTimer;
|
|
453
|
+
let anchorTimer;
|
|
454
|
+
let anchorCheckRunning = false;
|
|
455
|
+
|
|
456
|
+
const finish = () => {
|
|
457
|
+
if (settled) return;
|
|
458
|
+
settled = true;
|
|
459
|
+
clearTimeout(timeoutTimer);
|
|
460
|
+
clearTimeout(hardTimer);
|
|
461
|
+
clearTimeout(killTimer);
|
|
462
|
+
clearTimeout(termTimer);
|
|
463
|
+
clearInterval(anchorTimer);
|
|
464
|
+
abortHandle.cleanup();
|
|
465
|
+
resolvePromise({
|
|
466
|
+
code: resultMessage?.code ?? null,
|
|
467
|
+
signal: resultMessage?.signal ?? null,
|
|
468
|
+
timedOut,
|
|
469
|
+
aborted,
|
|
470
|
+
rawOverflow,
|
|
471
|
+
overflowStream,
|
|
472
|
+
bootstrapError,
|
|
473
|
+
stdout: Buffer.concat(chunks.stdout),
|
|
474
|
+
stderr: Buffer.concat(chunks.stderr),
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
const stop = () => {
|
|
478
|
+
if (stopping) return;
|
|
479
|
+
stopping = true;
|
|
480
|
+
try { child?.send({ version: BOOTSTRAP_VERSION, type: 'cancel', nonce }, () => {}); } catch {}
|
|
481
|
+
termTimer = setTimeout(() => { try { child?.kill('SIGTERM'); } catch {} }, KILL_GRACE_MS);
|
|
482
|
+
termTimer.unref?.();
|
|
483
|
+
killTimer = setTimeout(() => { try { child?.kill('SIGKILL'); } catch {} }, BOOTSTRAP_KILL_GRACE_MS);
|
|
484
|
+
killTimer.unref?.();
|
|
485
|
+
};
|
|
486
|
+
const collect = stream => chunk => {
|
|
487
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
488
|
+
const available = Math.max(0, rawLimit - rawBytes);
|
|
489
|
+
const accepted = buffer.subarray(0, available);
|
|
490
|
+
if (accepted.length > 0) chunks[stream].push(accepted);
|
|
491
|
+
rawBytes += accepted.length;
|
|
492
|
+
if (accepted.length < buffer.length) {
|
|
493
|
+
rawOverflow = true;
|
|
494
|
+
overflowStream = stream;
|
|
495
|
+
stop();
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const malformed = () => { bootstrapError = true; stop(); };
|
|
499
|
+
const onMessage = async message => {
|
|
500
|
+
if (!message || message.version !== BOOTSTRAP_VERSION || message.nonce !== nonce) return malformed();
|
|
501
|
+
if (message.type === 'ready') {
|
|
502
|
+
if (!exactMessage(message, ['version', 'type', 'nonce']) || launched) return malformed();
|
|
503
|
+
launched = true;
|
|
504
|
+
if (!await anchorsStillValid(prepared, data)) return malformed();
|
|
505
|
+
try { child.send({ version: BOOTSTRAP_VERSION, type: 'launch', nonce }, error => { if (error) malformed(); }); } catch { malformed(); }
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (message.type === 'result') {
|
|
509
|
+
if (!exactMessage(message, ['version', 'type', 'nonce', 'code', 'signal']) || resultMessage) return malformed();
|
|
510
|
+
if (!(message.code === null || Number.isInteger(message.code)) || !(message.signal === null || typeof message.signal === 'string')) return malformed();
|
|
511
|
+
resultMessage = Object.freeze({ code: message.code, signal: message.signal });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (message.type === 'failed') {
|
|
515
|
+
if (!exactMessage(message, ['version', 'type', 'nonce', 'reason'])) return malformed();
|
|
516
|
+
bootstrapError = true;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
return malformed();
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (aborted) {
|
|
523
|
+
finish();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
child = spawn(process.execPath, [BOOTSTRAP_PATH, BOOTSTRAP_FLAG], {
|
|
528
|
+
cwd: prepared.cwd,
|
|
529
|
+
env: Object.create(null),
|
|
530
|
+
shell: false,
|
|
531
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
532
|
+
windowsHide: true,
|
|
533
|
+
});
|
|
534
|
+
} catch {
|
|
535
|
+
bootstrapError = true;
|
|
536
|
+
finish();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
child.stdout?.on('data', collect('stdout'));
|
|
540
|
+
child.stderr?.on('data', collect('stderr'));
|
|
541
|
+
child.on('message', onMessage);
|
|
542
|
+
child.once('error', () => { bootstrapError = true; finish(); });
|
|
543
|
+
child.once('close', code => {
|
|
544
|
+
if (code !== 0 || !resultMessage) bootstrapError = true;
|
|
545
|
+
finish();
|
|
546
|
+
});
|
|
547
|
+
anchorTimer = setInterval(async () => {
|
|
548
|
+
if (anchorCheckRunning || settled || stopping) return;
|
|
549
|
+
anchorCheckRunning = true;
|
|
550
|
+
try {
|
|
551
|
+
if (!await anchorsStillValid(prepared, data)) {
|
|
552
|
+
bootstrapError = true;
|
|
553
|
+
stop();
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
anchorCheckRunning = false;
|
|
557
|
+
}
|
|
558
|
+
}, 5);
|
|
559
|
+
anchorTimer.unref?.();
|
|
560
|
+
abortHandle.onAbort(() => { aborted = true; stop(); });
|
|
561
|
+
child.send({
|
|
562
|
+
version: BOOTSTRAP_VERSION,
|
|
563
|
+
type: 'init',
|
|
564
|
+
nonce,
|
|
565
|
+
worktree: data.worktree,
|
|
566
|
+
cwd: prepared.cwd,
|
|
567
|
+
executable: prepared.executable,
|
|
568
|
+
args: prepared.args,
|
|
569
|
+
environment: data.environment,
|
|
570
|
+
identities: data.identities,
|
|
571
|
+
...(data.execution === 'argv' ? { execution: 'argv', invocation: data.invocation } : {}),
|
|
572
|
+
}, error => { if (error) malformed(); });
|
|
573
|
+
timeoutTimer = setTimeout(() => { timedOut = true; stop(); }, prepared.timeoutMs);
|
|
574
|
+
timeoutTimer.unref?.();
|
|
575
|
+
hardTimer = setTimeout(() => { bootstrapError = true; stop(); finish(); }, prepared.timeoutMs + HARD_STOP_MS);
|
|
576
|
+
hardTimer.unref?.();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function utf8Prefix(buffer, maximumBytes) {
|
|
581
|
+
const bounded = buffer.subarray(0, maximumBytes);
|
|
582
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
583
|
+
for (let length = bounded.length; length >= 0; length -= 1) {
|
|
584
|
+
try { return decoder.decode(bounded.subarray(0, length)); } catch {}
|
|
585
|
+
}
|
|
586
|
+
return '';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function sensitiveEnvironmentValues(environment) {
|
|
590
|
+
return [...new Set(Object.entries(environment)
|
|
591
|
+
.filter(([key, value]) => SENSITIVE_ENVIRONMENT_KEY.test(key) && typeof value === 'string' && value.length > 0)
|
|
592
|
+
.map(([, value]) => value))]
|
|
593
|
+
.sort((left, right) => right.length - left.length);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function unicodeSplitBoundaries(value) {
|
|
597
|
+
const boundaries = [];
|
|
598
|
+
let offset = 0;
|
|
599
|
+
for (const character of value) {
|
|
600
|
+
offset += character.length;
|
|
601
|
+
if (offset < value.length) boundaries.push(offset);
|
|
602
|
+
}
|
|
603
|
+
return boundaries;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function greatestPrefixMatch(value, secret, boundaries) {
|
|
607
|
+
let lower = 0;
|
|
608
|
+
let upper = boundaries.length - 1;
|
|
609
|
+
let match = -1;
|
|
610
|
+
while (lower <= upper) {
|
|
611
|
+
const middle = Math.floor((lower + upper) / 2);
|
|
612
|
+
if (value.includes(secret.slice(0, boundaries[middle]))) {
|
|
613
|
+
match = middle;
|
|
614
|
+
lower = middle + 1;
|
|
615
|
+
} else upper = middle - 1;
|
|
616
|
+
}
|
|
617
|
+
return match;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function leastSuffixMatch(value, secret, boundaries) {
|
|
621
|
+
let lower = 0;
|
|
622
|
+
let upper = boundaries.length - 1;
|
|
623
|
+
let match = -1;
|
|
624
|
+
while (lower <= upper) {
|
|
625
|
+
const middle = Math.floor((lower + upper) / 2);
|
|
626
|
+
if (value.includes(secret.slice(boundaries[middle]))) {
|
|
627
|
+
match = middle;
|
|
628
|
+
upper = middle - 1;
|
|
629
|
+
} else lower = middle + 1;
|
|
630
|
+
}
|
|
631
|
+
return match;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function collectCrossStreamFragments(first, second, secret, firstFragments, secondFragments) {
|
|
635
|
+
const boundaries = unicodeSplitBoundaries(secret);
|
|
636
|
+
if (boundaries.length === 0) return;
|
|
637
|
+
const longestPrefix = greatestPrefixMatch(first, secret, boundaries);
|
|
638
|
+
const longestSuffix = leastSuffixMatch(second, secret, boundaries);
|
|
639
|
+
if (longestPrefix < 0 || longestSuffix < 0 || longestSuffix > longestPrefix) return;
|
|
640
|
+
firstFragments.add(secret.slice(0, boundaries[longestSuffix]));
|
|
641
|
+
secondFragments.add(secret.slice(boundaries[longestPrefix]));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function secretPrefixMatcher(secret) {
|
|
645
|
+
const characters = [...secret];
|
|
646
|
+
const fallback = new Array(characters.length).fill(0);
|
|
647
|
+
for (let index = 1, matched = 0; index < characters.length; index += 1) {
|
|
648
|
+
while (matched > 0 && characters[index] !== characters[matched]) matched = fallback[matched - 1];
|
|
649
|
+
if (characters[index] === characters[matched]) matched += 1;
|
|
650
|
+
fallback[index] = matched;
|
|
651
|
+
}
|
|
652
|
+
return Object.freeze({ characters: Object.freeze(characters), fallback: Object.freeze(fallback) });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function candidateEndPositions(characters) {
|
|
656
|
+
const positions = [characters.length];
|
|
657
|
+
let cursor = characters.length;
|
|
658
|
+
let framing = 0;
|
|
659
|
+
while (cursor > 0 && RESULT_FRAMING.test(characters[cursor - 1])) {
|
|
660
|
+
framing += 1;
|
|
661
|
+
if (framing > MAX_RESULT_FRAMING) return Object.freeze({ excessive: true, positions: Object.freeze([]) });
|
|
662
|
+
cursor -= 1;
|
|
663
|
+
positions.push(cursor);
|
|
664
|
+
}
|
|
665
|
+
return Object.freeze({ excessive: false, positions: Object.freeze(positions) });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function prefixStatesAtPositions(characters, matcher, positions) {
|
|
669
|
+
const maximumPrefix = matcher.characters.length - 1;
|
|
670
|
+
const earliestEnd = positions[positions.length - 1];
|
|
671
|
+
const absoluteEnd = positions[0];
|
|
672
|
+
const start = Math.max(0, earliestEnd - maximumPrefix);
|
|
673
|
+
const required = new Set(positions);
|
|
674
|
+
const states = new Map();
|
|
675
|
+
let matched = 0;
|
|
676
|
+
if (required.has(start)) states.set(start, matched);
|
|
677
|
+
for (let index = start; index < absoluteEnd; index += 1) {
|
|
678
|
+
const character = characters[index];
|
|
679
|
+
while (matched > 0 && character !== matcher.characters[matched]) matched = matcher.fallback[matched - 1];
|
|
680
|
+
if (character === matcher.characters[matched]) matched += 1;
|
|
681
|
+
if (matched === matcher.characters.length) matched = matcher.fallback[matched - 1];
|
|
682
|
+
if (required.has(index + 1)) states.set(index + 1, matched);
|
|
683
|
+
}
|
|
684
|
+
return states;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function hasCredibleSecretPrefix(value, matchers) {
|
|
688
|
+
if (matchers.length === 0) return false;
|
|
689
|
+
const characters = [...value];
|
|
690
|
+
const candidates = candidateEndPositions(characters);
|
|
691
|
+
if (candidates.excessive) return true;
|
|
692
|
+
|
|
693
|
+
for (const matcher of matchers) {
|
|
694
|
+
if (matcher.characters.length < 2) continue;
|
|
695
|
+
const states = prefixStatesAtPositions(characters, matcher, candidates.positions);
|
|
696
|
+
for (const candidateEnd of candidates.positions) {
|
|
697
|
+
let matched = states.get(candidateEnd) ?? 0;
|
|
698
|
+
while (matched > 0) {
|
|
699
|
+
const candidateStart = candidateEnd - matched;
|
|
700
|
+
if (candidateStart === 0 || RESULT_FRAMING.test(characters[candidateStart - 1])) return true;
|
|
701
|
+
matched = matcher.fallback[matched - 1];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function analyzeWholeOutput(decoded, environment) {
|
|
709
|
+
const stdoutFragments = new Set();
|
|
710
|
+
const stderrFragments = new Set();
|
|
711
|
+
const secrets = sensitiveEnvironmentValues(environment);
|
|
712
|
+
const prefixMatchers = secrets.map(secretPrefixMatcher);
|
|
713
|
+
for (const secret of secrets) {
|
|
714
|
+
collectCrossStreamFragments(decoded.stdout, decoded.stderr, secret, stdoutFragments, stderrFragments);
|
|
715
|
+
collectCrossStreamFragments(decoded.stderr, decoded.stdout, secret, stderrFragments, stdoutFragments);
|
|
716
|
+
}
|
|
717
|
+
const redacted = redactSecrets(decoded, { environment });
|
|
718
|
+
return Object.freeze({
|
|
719
|
+
sensitive: redacted.stdout !== decoded.stdout || redacted.stderr !== decoded.stderr
|
|
720
|
+
|| stdoutFragments.size > 0 || stderrFragments.size > 0
|
|
721
|
+
|| hasCredibleSecretPrefix(decoded.stdout, prefixMatchers)
|
|
722
|
+
|| hasCredibleSecretPrefix(decoded.stderr, prefixMatchers),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function actualTruncation(raw, prepared) {
|
|
727
|
+
const stdoutLimit = Math.min(prepared.maxStreamOutputBytes, prepared.maxOutputBytes);
|
|
728
|
+
const stdoutAccepted = Math.min(raw.stdout.length, stdoutLimit);
|
|
729
|
+
const stderrLimit = Math.min(prepared.maxStreamOutputBytes, prepared.maxOutputBytes - stdoutAccepted);
|
|
730
|
+
return Object.freeze({
|
|
731
|
+
stdout: raw.stdout.length > stdoutLimit,
|
|
732
|
+
stderr: raw.stderr.length > stderrLimit,
|
|
733
|
+
combined: raw.stdout.length + raw.stderr.length > prepared.maxOutputBytes,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function sanitizeAndLimit(raw, prepared, environment) {
|
|
738
|
+
if (raw.rawOverflow) {
|
|
739
|
+
return Object.freeze({
|
|
740
|
+
stdout: '',
|
|
741
|
+
stderr: '',
|
|
742
|
+
redacted: false,
|
|
743
|
+
suppressed: true,
|
|
744
|
+
truncated: Object.freeze({ stdout: true, stderr: true, combined: true }),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const decoded = {
|
|
748
|
+
stdout: raw.bootstrapError ? '' : utf8Prefix(raw.stdout, raw.stdout.length),
|
|
749
|
+
stderr: raw.bootstrapError ? '' : utf8Prefix(raw.stderr, raw.stderr.length),
|
|
750
|
+
};
|
|
751
|
+
const analysis = analyzeWholeOutput(decoded, environment);
|
|
752
|
+
const stdoutLimit = Math.min(prepared.maxStreamOutputBytes, prepared.maxOutputBytes);
|
|
753
|
+
const stdoutAccepted = Math.min(raw.stdout.length, stdoutLimit);
|
|
754
|
+
const stderrLimit = Math.min(prepared.maxStreamOutputBytes, prepared.maxOutputBytes - stdoutAccepted);
|
|
755
|
+
const stdout = utf8Prefix(raw.stdout, stdoutLimit);
|
|
756
|
+
const stderr = utf8Prefix(raw.stderr, stderrLimit);
|
|
757
|
+
const suppressed = analysis.sensitive || raw.bootstrapError;
|
|
758
|
+
return Object.freeze({
|
|
759
|
+
stdout: suppressed ? '' : stdout,
|
|
760
|
+
stderr: suppressed ? '' : stderr,
|
|
761
|
+
redacted: analysis.sensitive,
|
|
762
|
+
suppressed,
|
|
763
|
+
truncated: actualTruncation(raw, prepared),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function commandStatus(raw, truncated) {
|
|
768
|
+
if (raw.aborted) return 'aborted';
|
|
769
|
+
if (raw.timedOut) return 'timeout';
|
|
770
|
+
if (truncated.stdout || truncated.stderr || truncated.combined) return 'output-overflow';
|
|
771
|
+
if (raw.bootstrapError) return 'bootstrap-error';
|
|
772
|
+
if (raw.signal) return 'signal';
|
|
773
|
+
if (raw.code !== 0) return 'nonzero';
|
|
774
|
+
return 'success';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export async function runCommand(contractInput, requestInput, optionsInput = {}) {
|
|
778
|
+
let abortHandle;
|
|
779
|
+
try {
|
|
780
|
+
const options = capture(optionsInput, new Set(['signal']), [], 'invalid-options');
|
|
781
|
+
abortHandle = captureAbortSignal(options.signal);
|
|
782
|
+
const prepared = await prepareCommand(contractInput, requestInput);
|
|
783
|
+
const data = preparedData.get(prepared);
|
|
784
|
+
if (!data) fail('invalid-prepared-command');
|
|
785
|
+
const raw = await executeAnchored(prepared, data, abortHandle);
|
|
786
|
+
const output = sanitizeAndLimit(raw, prepared, data.environment);
|
|
787
|
+
return Object.freeze({
|
|
788
|
+
status: commandStatus(raw, output.truncated),
|
|
789
|
+
code: raw.code,
|
|
790
|
+
signal: raw.signal,
|
|
791
|
+
timedOut: raw.timedOut,
|
|
792
|
+
aborted: raw.aborted,
|
|
793
|
+
stdout: output.stdout,
|
|
794
|
+
stderr: output.stderr,
|
|
795
|
+
redacted: output.redacted,
|
|
796
|
+
suppressed: output.suppressed,
|
|
797
|
+
truncated: output.truncated,
|
|
798
|
+
});
|
|
799
|
+
} catch (error) {
|
|
800
|
+
abortHandle?.cleanup();
|
|
801
|
+
throw sanitizedFailure(error);
|
|
802
|
+
}
|
|
803
|
+
}
|