@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,282 @@
|
|
|
1
|
+
import { verifyApproval } from './approvals.js';
|
|
2
|
+
|
|
3
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
4
|
+
const ACTION = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
5
|
+
const HUMAN_GATED = new Set([
|
|
6
|
+
'activation', 'scope-change', 'external-write', 'dependency.install',
|
|
7
|
+
'visual-baseline', 'visual-baseline.accept', 'merge', 'git.merge', 'deploy', 'deploy.execute',
|
|
8
|
+
'jira.final-state', 'docs.publish', 'publication', 'final-delivery',
|
|
9
|
+
]);
|
|
10
|
+
const envelopes = new WeakSet();
|
|
11
|
+
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
12
|
+
|
|
13
|
+
export class AuthorityPolicyError extends Error {
|
|
14
|
+
constructor(reason = 'invalid-authority') {
|
|
15
|
+
super(reason === 'overlapping-owned-paths'
|
|
16
|
+
? 'Authority contains overlapping owned paths.'
|
|
17
|
+
: 'Authority policy input is invalid.');
|
|
18
|
+
this.name = 'AuthorityPolicyError';
|
|
19
|
+
this.code = 'ERR_AUTHORITY_POLICY';
|
|
20
|
+
this.safeMessage = this.message;
|
|
21
|
+
this.details = Object.freeze({ reason });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fail(reason) {
|
|
26
|
+
throw new AuthorityPolicyError(reason);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function capture(value, allowed, required, reason) {
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(reason);
|
|
31
|
+
let keys;
|
|
32
|
+
try { keys = Reflect.ownKeys(value); } catch { fail(reason); }
|
|
33
|
+
if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail(reason);
|
|
34
|
+
const result = Object.create(null);
|
|
35
|
+
try {
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
38
|
+
if (!descriptor?.enumerable) fail(reason);
|
|
39
|
+
result[key] = value[key];
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof AuthorityPolicyError) throw error;
|
|
43
|
+
fail(reason);
|
|
44
|
+
}
|
|
45
|
+
if (required.some(key => !Object.hasOwn(result, key))) fail(reason);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function array(value, limit, reason) {
|
|
50
|
+
if (!Array.isArray(value)) fail(reason);
|
|
51
|
+
const result = [];
|
|
52
|
+
try {
|
|
53
|
+
const length = value.length;
|
|
54
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > limit) fail(reason);
|
|
55
|
+
for (let index = 0; index < length; index += 1) {
|
|
56
|
+
if (!Object.hasOwn(value, index)) fail(reason);
|
|
57
|
+
result.push(value[index]);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof AuthorityPolicyError) throw error;
|
|
61
|
+
fail(reason);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validId(value) {
|
|
67
|
+
return typeof value === 'string' && value.length <= 64 && ID.test(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function uniqueStrings(value, pattern, limit, reason) {
|
|
71
|
+
const values = array(value, limit, reason);
|
|
72
|
+
if (values.some(item => typeof item !== 'string' || item.length > 100 || !pattern.test(item))) fail(reason);
|
|
73
|
+
if (new Set(values).size !== values.length) fail(reason);
|
|
74
|
+
return values.sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function canonicalPath(value) {
|
|
78
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 500) fail('invalid-path');
|
|
79
|
+
if (value.includes('\\')) fail('invalid-path');
|
|
80
|
+
const normalized = value.normalize('NFKC');
|
|
81
|
+
if (normalized !== value && /[/\\:]/.test(normalized)) fail('invalid-path');
|
|
82
|
+
if (/[\\:]/.test(normalized)) fail('invalid-path');
|
|
83
|
+
const parts = normalized.split('/');
|
|
84
|
+
if (
|
|
85
|
+
normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized) || normalized.endsWith('/')
|
|
86
|
+
|| normalized.includes('//') || /[\u0000\r\n]/.test(normalized)
|
|
87
|
+
|| parts.some(part => (
|
|
88
|
+
part === '' || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
|
|
89
|
+
|| WINDOWS_RESERVED.test(part)
|
|
90
|
+
))
|
|
91
|
+
) fail('invalid-path');
|
|
92
|
+
return parts.join('/');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function containsPath(parent, child) {
|
|
96
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canonicalOwnedPaths(value) {
|
|
100
|
+
const paths = array(value, 256, 'invalid-owned-paths').map(canonicalPath).sort();
|
|
101
|
+
const folded = paths.map(path => path.toUpperCase().toLowerCase().normalize('NFKC')).sort();
|
|
102
|
+
if (new Set(folded).size !== folded.length) fail('overlapping-owned-paths');
|
|
103
|
+
for (let left = 0; left < folded.length; left += 1) {
|
|
104
|
+
for (let right = left + 1; right < folded.length; right += 1) {
|
|
105
|
+
if (containsPath(folded[left], folded[right])) {
|
|
106
|
+
throw new AuthorityPolicyError('overlapping-owned-paths');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return paths;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function captureProviders(value) {
|
|
114
|
+
const providers = array(value, 64, 'invalid-providers').map(item => {
|
|
115
|
+
const provider = capture(item, new Set(['id', 'mode', 'capabilities']), ['id', 'mode', 'capabilities'], 'invalid-provider');
|
|
116
|
+
if (!validId(provider.id) || !['disabled', 'read-only', 'read-write-with-approval'].includes(provider.mode)) fail('invalid-provider');
|
|
117
|
+
const capabilities = uniqueStrings(provider.capabilities, ID, 64, 'invalid-provider');
|
|
118
|
+
return Object.freeze({ id: provider.id, mode: provider.mode, capabilities: Object.freeze(capabilities) });
|
|
119
|
+
}).sort((left, right) => (left.id < right.id ? -1 : (left.id > right.id ? 1 : 0)));
|
|
120
|
+
if (new Set(providers.map(provider => provider.id)).size !== providers.length) fail('invalid-providers');
|
|
121
|
+
return providers;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function authorityMessage(reason) {
|
|
125
|
+
if (reason === 'action-ceiling' || reason === 'path-ceiling' || reason === 'provider-ceiling' || reason === 'command-ceiling') {
|
|
126
|
+
const error = new AuthorityPolicyError(reason);
|
|
127
|
+
error.message = 'Delegated authority exceeds the authority ceiling.';
|
|
128
|
+
error.safeMessage = error.message;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
fail(reason);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createAuthorityEnvelope(input) {
|
|
135
|
+
try {
|
|
136
|
+
const value = capture(input, new Set([
|
|
137
|
+
'actorId', 'principal', 'role', 'actions', 'ownedPaths', 'providers', 'commands',
|
|
138
|
+
]), ['actorId', 'principal', 'actions', 'ownedPaths', 'providers', 'commands'], 'invalid-authority');
|
|
139
|
+
if (!validId(value.actorId) || !['agent', 'human'].includes(value.principal)) fail('invalid-principal');
|
|
140
|
+
if (value.role !== undefined && (!validId(value.role))) fail('invalid-role-label');
|
|
141
|
+
const actions = uniqueStrings(value.actions, ACTION, 128, 'invalid-actions');
|
|
142
|
+
const ownedPaths = canonicalOwnedPaths(value.ownedPaths);
|
|
143
|
+
const providers = captureProviders(value.providers);
|
|
144
|
+
const commands = uniqueStrings(value.commands, ID, 64, 'invalid-commands');
|
|
145
|
+
const envelope = Object.freeze({
|
|
146
|
+
actorId: value.actorId,
|
|
147
|
+
principal: value.principal,
|
|
148
|
+
...(value.role === undefined ? {} : { role: value.role }),
|
|
149
|
+
actions: Object.freeze(actions),
|
|
150
|
+
ownedPaths: Object.freeze(ownedPaths),
|
|
151
|
+
providers: Object.freeze(providers),
|
|
152
|
+
commands: Object.freeze(commands),
|
|
153
|
+
});
|
|
154
|
+
envelopes.add(envelope);
|
|
155
|
+
return envelope;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof AuthorityPolicyError) throw error;
|
|
158
|
+
fail('invalid-authority');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function providerWithin(child, parent) {
|
|
163
|
+
const ceiling = parent.providers.find(provider => provider.id === child.id);
|
|
164
|
+
if (!ceiling) return false;
|
|
165
|
+
const modes = { disabled: 0, 'read-only': 1, 'read-write-with-approval': 2 };
|
|
166
|
+
return modes[child.mode] <= modes[ceiling.mode]
|
|
167
|
+
&& child.capabilities.every(capability => ceiling.capabilities.includes(capability));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function grant(childInput, actionsInput, parent) {
|
|
171
|
+
try {
|
|
172
|
+
if (!envelopes.has(parent)) fail('invalid-parent-authority');
|
|
173
|
+
const child = capture(childInput, new Set([
|
|
174
|
+
'actorId', 'principal', 'role', 'ownedPaths', 'providers', 'commands',
|
|
175
|
+
]), ['actorId'], 'invalid-child-authority');
|
|
176
|
+
const actions = uniqueStrings(actionsInput, ACTION, 128, 'invalid-actions');
|
|
177
|
+
if (!actions.every(action => parent.actions.includes(action))) authorityMessage('action-ceiling');
|
|
178
|
+
const candidate = createAuthorityEnvelope({
|
|
179
|
+
actorId: child.actorId,
|
|
180
|
+
principal: child.principal ?? 'agent',
|
|
181
|
+
...(child.role === undefined ? {} : { role: child.role }),
|
|
182
|
+
actions,
|
|
183
|
+
ownedPaths: child.ownedPaths ?? [],
|
|
184
|
+
providers: child.providers ?? [],
|
|
185
|
+
commands: child.commands ?? [],
|
|
186
|
+
});
|
|
187
|
+
if (candidate.principal === 'human' && parent.principal !== 'human') authorityMessage('action-ceiling');
|
|
188
|
+
if (!candidate.ownedPaths.every(path => parent.ownedPaths.some(parentPath => containsPath(parentPath, path)))) authorityMessage('path-ceiling');
|
|
189
|
+
if (!candidate.providers.every(provider => providerWithin(provider, parent))) authorityMessage('provider-ceiling');
|
|
190
|
+
if (!candidate.commands.every(command => parent.commands.includes(command))) authorityMessage('command-ceiling');
|
|
191
|
+
return candidate;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof AuthorityPolicyError) throw error;
|
|
194
|
+
fail('invalid-grant');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function decision(value, policyId, reason) {
|
|
199
|
+
return Object.freeze({ decision: value, policyId, reason });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function approvalResource(request) {
|
|
203
|
+
if (request.action === 'provider.write') return `${request.providerId}:${request.capability}:${request.resource}`;
|
|
204
|
+
return request.resource;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function withApproval(authority, request, options, policyId) {
|
|
208
|
+
if (!options.approval || !options.approvalRegistry || !options.expectedApproverId || options.nowMs === undefined) {
|
|
209
|
+
return decision('approval-required', policyId, 'human-approval-required');
|
|
210
|
+
}
|
|
211
|
+
const verified = verifyApproval(options.approval, {
|
|
212
|
+
subjectId: authority.actorId,
|
|
213
|
+
action: request.action,
|
|
214
|
+
resource: approvalResource(request),
|
|
215
|
+
policyId,
|
|
216
|
+
}, {
|
|
217
|
+
registry: options.approvalRegistry,
|
|
218
|
+
expectedApproverId: options.expectedApproverId,
|
|
219
|
+
requireHumanApprover: true,
|
|
220
|
+
requireSingleUse: true,
|
|
221
|
+
nowMs: options.nowMs,
|
|
222
|
+
});
|
|
223
|
+
return verified.valid
|
|
224
|
+
? decision('allow', policyId, 'approved')
|
|
225
|
+
: decision('approval-required', policyId, verified.reason);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function evaluateAuthority(authority, requestInput, optionsInput = {}) {
|
|
229
|
+
try {
|
|
230
|
+
if (!envelopes.has(authority)) fail('invalid-authority');
|
|
231
|
+
const request = capture(requestInput, new Set([
|
|
232
|
+
'actorId', 'action', 'resource', 'providerId', 'capability', 'commandId', 'subjectId',
|
|
233
|
+
]), ['actorId', 'action', 'resource'], 'invalid-request');
|
|
234
|
+
const options = capture(optionsInput, new Set([
|
|
235
|
+
'approval', 'approvalRegistry', 'expectedApproverId', 'nowMs',
|
|
236
|
+
]), [], 'invalid-options');
|
|
237
|
+
if (!validId(request.actorId) || request.actorId !== authority.actorId) return decision('deny', 'authority.actor-binding', 'actor-mismatch');
|
|
238
|
+
if (typeof request.action !== 'string' || !ACTION.test(request.action) || request.action.length > 100) fail('invalid-action');
|
|
239
|
+
if (typeof request.resource !== 'string' || request.resource.length === 0 || request.resource.length > 500 || /[\u0000\r\n]/.test(request.resource)) fail('invalid-resource');
|
|
240
|
+
if (!authority.actions.includes(request.action)) return decision('deny', 'authority.explicit-action', 'action-not-granted');
|
|
241
|
+
|
|
242
|
+
if (request.action === 'file.read' || request.action === 'file.write') {
|
|
243
|
+
const path = canonicalPath(request.resource);
|
|
244
|
+
if (!authority.ownedPaths.some(owned => containsPath(owned, path))) {
|
|
245
|
+
return decision('deny', 'authority.file-ownership', 'resource-not-owned');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (request.action.startsWith('command.') || request.action === 'dependency.install') {
|
|
250
|
+
if (!validId(request.commandId) || request.resource !== request.commandId || !authority.commands.includes(request.commandId)) {
|
|
251
|
+
return decision('deny', 'authority.command-allowlist', 'command-not-allowlisted');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (request.action === 'provider.read' || request.action === 'provider.write') {
|
|
256
|
+
if (!validId(request.providerId) || !validId(request.capability)) fail('invalid-provider-request');
|
|
257
|
+
const provider = authority.providers.find(item => item.id === request.providerId);
|
|
258
|
+
if (!provider || !provider.capabilities.includes(request.capability) || provider.mode === 'disabled') {
|
|
259
|
+
return decision('deny', 'authority.provider-capability', 'provider-action-not-granted');
|
|
260
|
+
}
|
|
261
|
+
if (request.action === 'provider.write') {
|
|
262
|
+
if (provider.mode !== 'read-write-with-approval') return decision('deny', 'authority.provider-mode', 'provider-read-only');
|
|
263
|
+
return withApproval(authority, request, options, 'authority.external-write');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (request.action === 'approval.record') {
|
|
268
|
+
if (!validId(request.subjectId)) return decision('deny', 'authority.approval-subject', 'invalid-approval-subject');
|
|
269
|
+
if (request.subjectId === authority.actorId) {
|
|
270
|
+
return decision('deny', 'authority.separation-of-duties', 'self-approval-forbidden');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (HUMAN_GATED.has(request.action)) {
|
|
274
|
+
if (authority.principal === 'human') return decision('allow', `authority.human-gate.${request.action}`, 'human-authority');
|
|
275
|
+
return decision('approval-required', `authority.human-gate.${request.action}`, 'human-approval-required');
|
|
276
|
+
}
|
|
277
|
+
return decision('allow', 'authority.explicit-action', 'action-granted');
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error instanceof AuthorityPolicyError) throw error;
|
|
280
|
+
fail('invalid-policy-input');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const LIMIT_FIELDS = Object.freeze(['elapsedMs', 'agentTurns', 'retries', 'childCount', 'activeNodes', 'tokens', 'costUsd']);
|
|
2
|
+
const MONOTONIC_FIELDS = Object.freeze(['agentTurns', 'retries', 'childCount', 'tokens', 'costUsd']);
|
|
3
|
+
const snapshots = new WeakSet();
|
|
4
|
+
|
|
5
|
+
export class BudgetPolicyError extends Error {
|
|
6
|
+
constructor(reason = 'invalid-budget') {
|
|
7
|
+
super(reason === 'non-monotonic-observation'
|
|
8
|
+
? 'Budget observations must be monotonic.'
|
|
9
|
+
: 'Budget policy input is invalid.');
|
|
10
|
+
this.name = 'BudgetPolicyError';
|
|
11
|
+
this.code = 'ERR_BUDGET_POLICY';
|
|
12
|
+
this.safeMessage = this.message;
|
|
13
|
+
this.details = Object.freeze({ reason });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fail(reason) { throw new BudgetPolicyError(reason); }
|
|
18
|
+
|
|
19
|
+
function capture(value, allowed, required, reason) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(reason);
|
|
21
|
+
let keys;
|
|
22
|
+
try { keys = Reflect.ownKeys(value); } catch { fail(reason); }
|
|
23
|
+
if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail(reason);
|
|
24
|
+
const result = Object.create(null);
|
|
25
|
+
try {
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
28
|
+
if (!descriptor?.enumerable) fail(reason);
|
|
29
|
+
result[key] = value[key];
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof BudgetPolicyError) throw error;
|
|
33
|
+
fail(reason);
|
|
34
|
+
}
|
|
35
|
+
if (required.some(key => !Object.hasOwn(result, key))) fail(reason);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function integer(value, positive = false) {
|
|
40
|
+
if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0)) fail('invalid-integer-budget');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function decimal(value) {
|
|
45
|
+
const source = typeof value === 'number' && Number.isFinite(value) ? value.toString() : value;
|
|
46
|
+
if (typeof source !== 'string' || source.length === 0 || source.length > 64) fail('invalid-decimal-budget');
|
|
47
|
+
const match = /^(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(source);
|
|
48
|
+
if (!match) fail('invalid-decimal-budget');
|
|
49
|
+
const exponent = Number(match[3] ?? 0);
|
|
50
|
+
if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 100) fail('invalid-decimal-budget');
|
|
51
|
+
let digits = `${match[1]}${match[2] ?? ''}`.replace(/^0+(?=\d)/, '');
|
|
52
|
+
let scale = (match[2]?.length ?? 0) - exponent;
|
|
53
|
+
if (scale < 0) { digits += '0'.repeat(-scale); scale = 0; }
|
|
54
|
+
while (scale > 0 && digits.endsWith('0')) { digits = digits.slice(0, -1); scale -= 1; }
|
|
55
|
+
const coefficient = BigInt(digits || '0');
|
|
56
|
+
return { coefficient, scale, text: scale === 0
|
|
57
|
+
? coefficient.toString()
|
|
58
|
+
: `${coefficient.toString().padStart(scale + 1, '0').slice(0, -scale)}.${coefficient.toString().padStart(scale + 1, '0').slice(-scale)}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compareDecimal(leftValue, rightValue) {
|
|
62
|
+
const left = decimal(leftValue);
|
|
63
|
+
const right = decimal(rightValue);
|
|
64
|
+
const scale = Math.max(left.scale, right.scale);
|
|
65
|
+
const leftScaled = left.coefficient * (10n ** BigInt(scale - left.scale));
|
|
66
|
+
const rightScaled = right.coefficient * (10n ** BigInt(scale - right.scale));
|
|
67
|
+
return leftScaled === rightScaled ? 0 : (leftScaled < rightScaled ? -1 : 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function now(options) {
|
|
71
|
+
const value = options.nowMs ?? Date.now();
|
|
72
|
+
return integer(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeSnapshot(limits, usage, startedAtMs, observedAtMs) {
|
|
76
|
+
const result = Object.freeze({
|
|
77
|
+
limits: Object.freeze({ ...limits }),
|
|
78
|
+
usage: Object.freeze({ ...usage }),
|
|
79
|
+
startedAtMs,
|
|
80
|
+
observedAtMs,
|
|
81
|
+
});
|
|
82
|
+
snapshots.add(result);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createBudgetSnapshot(limitsInput, optionsInput = {}) {
|
|
87
|
+
try {
|
|
88
|
+
const limitsValue = capture(limitsInput, new Set(LIMIT_FIELDS), LIMIT_FIELDS, 'invalid-limits');
|
|
89
|
+
const options = capture(optionsInput, new Set(['nowMs']), [], 'invalid-options');
|
|
90
|
+
const limits = {};
|
|
91
|
+
for (const field of LIMIT_FIELDS) {
|
|
92
|
+
limits[field] = field === 'costUsd'
|
|
93
|
+
? decimal(limitsValue[field]).text
|
|
94
|
+
: integer(limitsValue[field], true);
|
|
95
|
+
}
|
|
96
|
+
if (compareDecimal(limits.costUsd, '0') <= 0) fail('invalid-decimal-budget');
|
|
97
|
+
const nowMs = now(options);
|
|
98
|
+
return makeSnapshot(limits, {
|
|
99
|
+
elapsedMs: 0, agentTurns: 0, retries: 0, childCount: 0, activeNodes: 0, tokens: 0, costUsd: '0',
|
|
100
|
+
}, nowMs, nowMs);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof BudgetPolicyError) throw error;
|
|
103
|
+
fail('invalid-budget');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function observeBudget(snapshot, observationInput, optionsInput = {}) {
|
|
108
|
+
try {
|
|
109
|
+
if (!snapshots.has(snapshot)) fail('untrusted-snapshot');
|
|
110
|
+
const observation = capture(observationInput, new Set(LIMIT_FIELDS.filter(field => field !== 'elapsedMs')), [], 'invalid-observation');
|
|
111
|
+
const options = capture(optionsInput, new Set(['nowMs']), [], 'invalid-options');
|
|
112
|
+
const observedAtMs = now(options);
|
|
113
|
+
if (observedAtMs < snapshot.observedAtMs) fail('non-monotonic-observation');
|
|
114
|
+
const usage = { ...snapshot.usage, elapsedMs: observedAtMs - snapshot.startedAtMs };
|
|
115
|
+
for (const field of Object.keys(observation)) {
|
|
116
|
+
const value = field === 'costUsd' ? decimal(observation[field]).text : integer(observation[field]);
|
|
117
|
+
if (MONOTONIC_FIELDS.includes(field)) {
|
|
118
|
+
const backwards = field === 'costUsd'
|
|
119
|
+
? compareDecimal(value, snapshot.usage[field]) < 0
|
|
120
|
+
: value < snapshot.usage[field];
|
|
121
|
+
if (backwards) fail('non-monotonic-observation');
|
|
122
|
+
}
|
|
123
|
+
usage[field] = value;
|
|
124
|
+
}
|
|
125
|
+
return makeSnapshot(snapshot.limits, usage, snapshot.startedAtMs, observedAtMs);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof BudgetPolicyError) throw error;
|
|
128
|
+
fail('invalid-budget-observation');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function evaluateBudget(snapshot) {
|
|
133
|
+
try {
|
|
134
|
+
if (!snapshots.has(snapshot)) fail('untrusted-snapshot');
|
|
135
|
+
const exhausted = LIMIT_FIELDS.filter(field => field === 'costUsd'
|
|
136
|
+
? compareDecimal(snapshot.usage[field], snapshot.limits[field]) >= 0
|
|
137
|
+
: snapshot.usage[field] >= snapshot.limits[field]);
|
|
138
|
+
if (exhausted.length === 0) return Object.freeze({
|
|
139
|
+
decision: 'allow', policyId: 'budget.available', reason: 'budget-available', exhausted: Object.freeze([]),
|
|
140
|
+
});
|
|
141
|
+
const firstReason = exhausted[0] === 'elapsedMs' ? 'elapsed-time-exhausted' : `${exhausted[0]}-exhausted`;
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
decision: 'deny', policyId: 'budget.exhausted', reason: firstReason, exhausted: Object.freeze(exhausted),
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof BudgetPolicyError) throw error;
|
|
147
|
+
fail('invalid-budget');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const VERSION = 1;
|
|
6
|
+
const STARTUP_TIMEOUT_MS = 1_000;
|
|
7
|
+
const KILL_GRACE_MS = 100;
|
|
8
|
+
const NONCE = /^[a-f0-9]{32}$/;
|
|
9
|
+
const IDENTITY = /^\d+$/;
|
|
10
|
+
const ACTION_ARG_LIMIT = 256;
|
|
11
|
+
const ENV_LIMIT = 256;
|
|
12
|
+
|
|
13
|
+
function exactObject(value, keys) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
+
&& Reflect.ownKeys(value).length === keys.length
|
|
16
|
+
&& keys.every(key => Object.hasOwn(value, key));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function boundedStrings(value, maximum, itemMaximum) {
|
|
20
|
+
return Array.isArray(value) && value.length <= maximum
|
|
21
|
+
&& value.every(item => typeof item === 'string' && item.length <= itemMaximum && !item.includes('\0'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validEnvironment(value) {
|
|
25
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
26
|
+
const keys = Reflect.ownKeys(value);
|
|
27
|
+
return keys.length <= ENV_LIMIT && keys.every(key => (
|
|
28
|
+
typeof key === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(key)
|
|
29
|
+
&& typeof value[key] === 'string' && value[key].length <= 32_768 && !value[key].includes('\0')
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function within(parent, child) {
|
|
34
|
+
const path = relative(parent, child);
|
|
35
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validIdentity(value) {
|
|
39
|
+
return exactObject(value, ['dev', 'ino']) && IDENTITY.test(value.dev) && IDENTITY.test(value.ino);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function captureInit(message) {
|
|
43
|
+
if (!exactObject(message, ['version', 'type', 'nonce', 'worktree', 'cwd', 'executable', 'args', 'environment', 'identities', ...(message?.execution === 'argv' ? ['execution', 'invocation'] : [])])) return null;
|
|
44
|
+
if (message.version !== VERSION || message.type !== 'init' || !NONCE.test(message.nonce)) return null;
|
|
45
|
+
if (![message.worktree, message.cwd, message.executable].every(value => (
|
|
46
|
+
typeof value === 'string' && value.length > 0 && value.length <= 1_024 && isAbsolute(value) && !value.includes('\0')
|
|
47
|
+
))) return null;
|
|
48
|
+
if (!boundedStrings(message.args, ACTION_ARG_LIMIT, 4_096) || !validEnvironment(message.environment)) return null;
|
|
49
|
+
if (!exactObject(message.identities, ['worktree', 'cwd', 'executable'])) return null;
|
|
50
|
+
if (!Object.values(message.identities).every(validIdentity)) return null;
|
|
51
|
+
if (message.execution === 'argv') {
|
|
52
|
+
const invocation = message.invocation;
|
|
53
|
+
if (!exactObject(invocation, ['target', 'parent', 'identity', 'parentIdentity'])
|
|
54
|
+
|| ![invocation.target, invocation.parent].every(path => typeof path === 'string'
|
|
55
|
+
&& path.length <= 1_024 && isAbsolute(path) && !/[\u0000\r\n]/.test(path))
|
|
56
|
+
|| dirname(message.executable) !== invocation.parent
|
|
57
|
+
|| !validIdentity(invocation.identity) || !validIdentity(invocation.parentIdentity)) return null;
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
version: VERSION,
|
|
61
|
+
execution: message.execution,
|
|
62
|
+
...(message.execution === 'argv' ? { invocation: Object.freeze({
|
|
63
|
+
target: message.invocation.target, parent: message.invocation.parent,
|
|
64
|
+
identity: Object.freeze({ ...message.invocation.identity }),
|
|
65
|
+
parentIdentity: Object.freeze({ ...message.invocation.parentIdentity }),
|
|
66
|
+
}) } : {}),
|
|
67
|
+
nonce: message.nonce,
|
|
68
|
+
worktree: message.worktree,
|
|
69
|
+
cwd: message.cwd,
|
|
70
|
+
executable: message.executable,
|
|
71
|
+
args: Object.freeze([...message.args]),
|
|
72
|
+
environment: Object.freeze(Object.assign(Object.create(null), message.environment)),
|
|
73
|
+
identities: Object.freeze({
|
|
74
|
+
worktree: Object.freeze({ ...message.identities.worktree }),
|
|
75
|
+
cwd: Object.freeze({ ...message.identities.cwd }),
|
|
76
|
+
executable: Object.freeze({ ...message.identities.executable }),
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sameIdentity(metadata, expected) {
|
|
82
|
+
return metadata.dev.toString() === expected.dev && metadata.ino.toString() === expected.ino;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function verifyAnchor(config) {
|
|
86
|
+
const [cwdMetadata, cwdReal, worktreeMetadata, worktreeReal, executableMetadata, executableReal] = await Promise.all([
|
|
87
|
+
lstat('.', { bigint: true }),
|
|
88
|
+
realpath('.'),
|
|
89
|
+
lstat(config.worktree, { bigint: true }),
|
|
90
|
+
realpath(config.worktree),
|
|
91
|
+
lstat(config.executable, { bigint: true }),
|
|
92
|
+
realpath(config.executable),
|
|
93
|
+
]);
|
|
94
|
+
let executableValid;
|
|
95
|
+
if (config.execution === 'argv') {
|
|
96
|
+
const invocation = config.invocation;
|
|
97
|
+
const [targetMetadata, targetReal, parentMetadata, parentReal] = await Promise.all([
|
|
98
|
+
lstat(invocation.target, { bigint: true }), realpath(invocation.target),
|
|
99
|
+
lstat(invocation.parent, { bigint: true }), realpath(invocation.parent),
|
|
100
|
+
]);
|
|
101
|
+
executableValid = (executableMetadata.isFile() || executableMetadata.isSymbolicLink())
|
|
102
|
+
&& executableReal === invocation.target && targetReal === invocation.target
|
|
103
|
+
&& targetMetadata.isFile() && !targetMetadata.isSymbolicLink() && (targetMetadata.mode & 0o111n) !== 0n
|
|
104
|
+
&& parentMetadata.isDirectory() && !parentMetadata.isSymbolicLink() && parentReal === invocation.parent
|
|
105
|
+
&& sameIdentity(executableMetadata, invocation.identity)
|
|
106
|
+
&& sameIdentity(targetMetadata, config.identities.executable)
|
|
107
|
+
&& sameIdentity(parentMetadata, invocation.parentIdentity);
|
|
108
|
+
} else {
|
|
109
|
+
executableValid = executableMetadata.isFile() && !executableMetadata.isSymbolicLink()
|
|
110
|
+
&& executableMetadata.nlink === 1n && executableReal === config.executable
|
|
111
|
+
&& sameIdentity(executableMetadata, config.identities.executable);
|
|
112
|
+
}
|
|
113
|
+
return cwdMetadata.isDirectory() && !cwdMetadata.isSymbolicLink()
|
|
114
|
+
&& worktreeMetadata.isDirectory() && !worktreeMetadata.isSymbolicLink()
|
|
115
|
+
&& cwdReal === config.cwd && worktreeReal === config.worktree
|
|
116
|
+
&& within(worktreeReal, cwdReal)
|
|
117
|
+
&& sameIdentity(cwdMetadata, config.identities.cwd)
|
|
118
|
+
&& sameIdentity(worktreeMetadata, config.identities.worktree)
|
|
119
|
+
&& executableValid;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function startBootstrap() {
|
|
123
|
+
let config;
|
|
124
|
+
let target;
|
|
125
|
+
let settled = false;
|
|
126
|
+
let killTimer;
|
|
127
|
+
let anchorTimer;
|
|
128
|
+
const startupTimer = setTimeout(() => shutdown('startup-timeout'), STARTUP_TIMEOUT_MS);
|
|
129
|
+
startupTimer.unref?.();
|
|
130
|
+
|
|
131
|
+
function send(type, fields = {}) {
|
|
132
|
+
if (process.connected) process.send({ version: VERSION, type, nonce: config?.nonce ?? null, ...fields }, () => {});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function terminateTarget() {
|
|
136
|
+
if (!target) return;
|
|
137
|
+
const signalTarget = signal => {
|
|
138
|
+
try {
|
|
139
|
+
if (process.platform !== 'win32' && Number.isSafeInteger(target.pid) && target.pid > 0) process.kill(-target.pid, signal);
|
|
140
|
+
else target.kill(signal);
|
|
141
|
+
} catch {}
|
|
142
|
+
};
|
|
143
|
+
signalTarget('SIGTERM');
|
|
144
|
+
killTimer = setTimeout(() => signalTarget('SIGKILL'), KILL_GRACE_MS);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function shutdown(reason, exitCode = 1) {
|
|
148
|
+
if (settled) return;
|
|
149
|
+
settled = true;
|
|
150
|
+
clearTimeout(startupTimer);
|
|
151
|
+
clearInterval(anchorTimer);
|
|
152
|
+
terminateTarget();
|
|
153
|
+
send('failed', { reason });
|
|
154
|
+
setTimeout(() => process.exit(exitCode), target ? KILL_GRACE_MS + 25 : 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function launch() {
|
|
158
|
+
if (!await verifyAnchor(config)) return shutdown('anchor-invalid');
|
|
159
|
+
try {
|
|
160
|
+
target = spawn(config.executable, config.args, {
|
|
161
|
+
env: config.environment,
|
|
162
|
+
detached: process.platform !== 'win32',
|
|
163
|
+
shell: false,
|
|
164
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
return shutdown('target-spawn-failed');
|
|
169
|
+
}
|
|
170
|
+
target.once('error', () => shutdown('target-spawn-failed'));
|
|
171
|
+
anchorTimer = setInterval(async () => {
|
|
172
|
+
try {
|
|
173
|
+
if (!await verifyAnchor(config)) shutdown('anchor-changed');
|
|
174
|
+
} catch {
|
|
175
|
+
shutdown('anchor-changed');
|
|
176
|
+
}
|
|
177
|
+
}, 5);
|
|
178
|
+
anchorTimer.unref?.();
|
|
179
|
+
target.once('close', (code, signal) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(killTimer);
|
|
183
|
+
clearInterval(anchorTimer);
|
|
184
|
+
send('result', {
|
|
185
|
+
code: Number.isInteger(code) ? code : null,
|
|
186
|
+
signal: typeof signal === 'string' ? signal : null,
|
|
187
|
+
});
|
|
188
|
+
process.disconnect?.();
|
|
189
|
+
process.exitCode = 0;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.on('message', async message => {
|
|
194
|
+
try {
|
|
195
|
+
if (!config) {
|
|
196
|
+
config = captureInit(message);
|
|
197
|
+
if (!config || !await verifyAnchor(config)) return shutdown('invalid-init');
|
|
198
|
+
clearTimeout(startupTimer);
|
|
199
|
+
send('ready');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!exactObject(message, ['version', 'type', 'nonce']) || message.version !== VERSION || message.nonce !== config.nonce) {
|
|
203
|
+
return shutdown('invalid-message');
|
|
204
|
+
}
|
|
205
|
+
if (message.type === 'launch' && !target) return launch();
|
|
206
|
+
if (message.type === 'cancel') return shutdown('cancelled');
|
|
207
|
+
return shutdown('invalid-message');
|
|
208
|
+
} catch {
|
|
209
|
+
return shutdown('internal-failure');
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
process.once('disconnect', () => shutdown('ipc-closed'));
|
|
213
|
+
// The foreground process group may receive a second signal while shutdown
|
|
214
|
+
// is waiting to kill the target group. Keep consuming it until cleanup ends.
|
|
215
|
+
process.on('SIGTERM', () => shutdown('terminated'));
|
|
216
|
+
process.on('SIGINT', () => shutdown('terminated'));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (process.argv[2] === '--agilno-command-bootstrap-v1' && typeof process.send === 'function') startBootstrap();
|