@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,169 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { assertGitClient } from '../git/client.js';
|
|
6
|
+
import { createApprovalReceipt, createApprovalRegistry } from '../policy/approvals.js';
|
|
7
|
+
import { createAuthorityEnvelope } from '../policy/authority.js';
|
|
8
|
+
import { runCommand } from '../policy/commands.js';
|
|
9
|
+
import { inspectPortableDependencies, bootstrapPortableDependencies } from './portable-dependencies.js';
|
|
10
|
+
|
|
11
|
+
const LOCKFILES = Object.freeze({
|
|
12
|
+
npm: ['package-lock.json', 'npm-shrinkwrap.json'],
|
|
13
|
+
pnpm: ['pnpm-lock.yaml'],
|
|
14
|
+
yarn: ['yarn.lock'],
|
|
15
|
+
bun: ['bun.lock', 'bun.lockb'],
|
|
16
|
+
});
|
|
17
|
+
const INSTALL_ARGS = Object.freeze({
|
|
18
|
+
npm: ['ci'],
|
|
19
|
+
pnpm: ['install', '--frozen-lockfile'],
|
|
20
|
+
yarn: ['install', '--frozen-lockfile'],
|
|
21
|
+
bun: ['install', '--frozen-lockfile'],
|
|
22
|
+
});
|
|
23
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
24
|
+
|
|
25
|
+
export class WorktreeBootstrapError extends Error {
|
|
26
|
+
constructor(reason) {
|
|
27
|
+
const messages = {
|
|
28
|
+
'invalid-input': 'Dependency setup input is invalid.',
|
|
29
|
+
'unsafe-checkout': 'The isolated checkout changed or is unsafe. Inspect the task before retrying.',
|
|
30
|
+
'missing-manifest': 'The isolated checkout has no safe package.json at its root.',
|
|
31
|
+
'missing-lockfile': 'Commit the project lockfile before preparing isolated dependencies.',
|
|
32
|
+
'ambiguous-lockfile': 'The project has multiple package manager lockfiles. Keep one matching the configured package manager.',
|
|
33
|
+
'manager-mismatch': 'The project lockfile does not match its configured package manager.',
|
|
34
|
+
'checkout-changed': 'Dependency setup changed tracked project files. Inspect the isolated checkout before continuing.',
|
|
35
|
+
'approval-required': 'Locked dependencies need separate approval before the Worker starts. Use an interactive rivet task command.',
|
|
36
|
+
'approval-declined': 'Dependency installation was declined. The Worker did not start.',
|
|
37
|
+
'install-failed': 'Dependency installation failed in the isolated checkout. Inspect the package-manager output and retry.',
|
|
38
|
+
};
|
|
39
|
+
super(messages[reason] ?? messages['invalid-input']);
|
|
40
|
+
this.name = 'WorktreeBootstrapError';
|
|
41
|
+
this.code = `ERR_WORKTREE_BOOTSTRAP_${reason.replaceAll('-', '_').toUpperCase()}`;
|
|
42
|
+
this.safeMessage = this.message;
|
|
43
|
+
this.details = Object.freeze({ reason });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fail(reason) { throw new WorktreeBootstrapError(reason); }
|
|
48
|
+
|
|
49
|
+
export function bootstrapInput(input) {
|
|
50
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)
|
|
51
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail('invalid-input');
|
|
52
|
+
const keys = Reflect.ownKeys(input);
|
|
53
|
+
const portable = Object.hasOwn(input, 'dependencies');
|
|
54
|
+
const expected = ['projectRoot', 'worktreePath', 'expectedCommit', 'expectedBranch', portable ? 'dependencies' : 'manager'];
|
|
55
|
+
if (keys.length !== expected.length || keys.some(key => typeof key !== 'string' || !expected.includes(key))) fail('invalid-input');
|
|
56
|
+
const value = Object.create(null);
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
59
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-input');
|
|
60
|
+
value[key] = descriptor.value;
|
|
61
|
+
}
|
|
62
|
+
if ([value.projectRoot, value.worktreePath].some(path => typeof path !== 'string' || !isAbsolute(path)
|
|
63
|
+
|| resolve(path) !== path || /[\u0000\r\n]/.test(path))
|
|
64
|
+
|| value.projectRoot === value.worktreePath
|
|
65
|
+
|| typeof value.expectedBranch !== 'string' || !value.expectedBranch || /[\u0000\r\n]/.test(value.expectedBranch)
|
|
66
|
+
|| !SHA.test(value.expectedCommit) || (!portable && !Object.hasOwn(LOCKFILES, value.manager))) fail('invalid-input');
|
|
67
|
+
return Object.freeze(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function safeFile(path) {
|
|
71
|
+
try {
|
|
72
|
+
const before = await lstat(path, { bigint: true });
|
|
73
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size < 1n) fail('unsafe-checkout');
|
|
74
|
+
const canonical = await realpath(path);
|
|
75
|
+
const after = await lstat(path, { bigint: true });
|
|
76
|
+
if (canonical !== path || !after.isFile() || after.isSymbolicLink()
|
|
77
|
+
|| before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size) fail('unsafe-checkout');
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error?.code === 'ENOENT') return false;
|
|
81
|
+
fail('unsafe-checkout');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function verifyBootstrapCheckout(value, gitClient) {
|
|
86
|
+
let source;
|
|
87
|
+
let isolated;
|
|
88
|
+
try {
|
|
89
|
+
[source, isolated] = await Promise.all([
|
|
90
|
+
gitClient.inspectRepository(value.projectRoot), gitClient.inspectRepository(value.worktreePath),
|
|
91
|
+
]);
|
|
92
|
+
} catch { fail('unsafe-checkout'); }
|
|
93
|
+
if (source.root !== value.projectRoot || isolated.root !== value.worktreePath
|
|
94
|
+
|| source.repositoryId !== isolated.repositoryId || source.gitCommonDir !== isolated.gitCommonDir
|
|
95
|
+
|| source.branch === isolated.branch
|
|
96
|
+
|| isolated.branch !== value.expectedBranch || isolated.headSha !== value.expectedCommit
|
|
97
|
+
|| isolated.detached || isolated.dirty) fail('unsafe-checkout');
|
|
98
|
+
const registered = (await gitClient.listWorktrees(value.projectRoot).catch(() => fail('unsafe-checkout')))
|
|
99
|
+
.filter(item => item.path === value.worktreePath && item.branch === value.expectedBranch);
|
|
100
|
+
if (registered.length !== 1 || registered[0].head !== value.expectedCommit) fail('unsafe-checkout');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function inspectWorktreeDependencies(input, options = {}) {
|
|
104
|
+
const value = bootstrapInput(input);
|
|
105
|
+
if (Object.hasOwn(value, 'dependencies')) return inspectPortableDependencies(value, options);
|
|
106
|
+
const gitClient = options.gitClient;
|
|
107
|
+
try { assertGitClient(gitClient); } catch { fail('invalid-input'); }
|
|
108
|
+
await verifyBootstrapCheckout(value, gitClient);
|
|
109
|
+
if (!(await safeFile(join(value.worktreePath, 'package.json')))) fail('missing-manifest');
|
|
110
|
+
const found = [];
|
|
111
|
+
for (const [manager, names] of Object.entries(LOCKFILES)) {
|
|
112
|
+
for (const name of names) if (await safeFile(join(value.worktreePath, name))) found.push({ manager, name });
|
|
113
|
+
}
|
|
114
|
+
if (found.length === 0) fail('missing-lockfile');
|
|
115
|
+
if (found.length > 1) fail('ambiguous-lockfile');
|
|
116
|
+
if (found[0].manager !== value.manager) fail('manager-mismatch');
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
projectRoot: value.projectRoot,
|
|
119
|
+
worktreePath: value.worktreePath,
|
|
120
|
+
expectedCommit: value.expectedCommit,
|
|
121
|
+
expectedBranch: value.expectedBranch,
|
|
122
|
+
manager: value.manager,
|
|
123
|
+
lockfile: found[0].name,
|
|
124
|
+
args: Object.freeze([...INSTALL_ARGS[value.manager]]),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function bootstrapWorktreeDependencies(input, options = {}) {
|
|
129
|
+
const value = bootstrapInput(input);
|
|
130
|
+
if (Object.hasOwn(value, 'dependencies')) return bootstrapPortableDependencies(value, options);
|
|
131
|
+
const plan = await inspectWorktreeDependencies(input, options);
|
|
132
|
+
if (typeof options.resolveCommandExecutable !== 'function' || typeof options.confirm !== 'function') fail('invalid-input');
|
|
133
|
+
const executable = await options.resolveCommandExecutable(plan.manager);
|
|
134
|
+
if (typeof executable !== 'string' || !isAbsolute(executable)) fail('invalid-input');
|
|
135
|
+
const approved = await options.confirm(Object.freeze({
|
|
136
|
+
manager: plan.manager, executable, args: plan.args, lockfile: plan.lockfile, worktreePath: plan.worktreePath,
|
|
137
|
+
}));
|
|
138
|
+
if (approved !== true) return Object.freeze({ status: 'declined' });
|
|
139
|
+
const current = await inspectWorktreeDependencies(input, options);
|
|
140
|
+
if (current.lockfile !== plan.lockfile || current.manager !== plan.manager) fail('unsafe-checkout');
|
|
141
|
+
const nowMs = Date.now();
|
|
142
|
+
const registry = createApprovalRegistry({ approvers: [{ id: 'human-owner', principal: 'human' }] });
|
|
143
|
+
const approval = createApprovalReceipt({
|
|
144
|
+
id: `approval-${randomUUID()}`,
|
|
145
|
+
approverId: 'human-owner', approverPrincipal: 'human', subjectId: 'rivet-bootstrap',
|
|
146
|
+
action: 'dependency.install', resource: `command:install:${plan.worktreePath}`,
|
|
147
|
+
policyId: 'dependency.install', decision: 'approved',
|
|
148
|
+
expiresAt: new Date(nowMs + 60_000).toISOString(), singleUse: true,
|
|
149
|
+
});
|
|
150
|
+
const result = await runCommand({
|
|
151
|
+
worktree: plan.worktreePath,
|
|
152
|
+
authority: createAuthorityEnvelope({
|
|
153
|
+
actorId: 'rivet-bootstrap', principal: 'agent', actions: ['dependency.install'],
|
|
154
|
+
ownedPaths: [], providers: [], commands: ['install'],
|
|
155
|
+
}),
|
|
156
|
+
commands: { install: {
|
|
157
|
+
executable, args: plan.args, action: 'dependency.install', elevated: true,
|
|
158
|
+
approvalPolicyId: 'dependency.install', approverId: 'human-owner',
|
|
159
|
+
} },
|
|
160
|
+
environment: { PATH: `${dirname(process.execPath)}:/usr/bin:/bin` },
|
|
161
|
+
timeoutMs: 10 * 60_000,
|
|
162
|
+
maxOutputBytes: 16 * 1024,
|
|
163
|
+
maxStreamOutputBytes: 16 * 1024,
|
|
164
|
+
}, { actorId: 'rivet-bootstrap', commandId: 'install', cwd: '.', approval, approvalRegistry: registry, nowMs },
|
|
165
|
+
{ signal: options.signal });
|
|
166
|
+
const after = await options.gitClient.inspectRepository(plan.worktreePath).catch(() => fail('checkout-changed'));
|
|
167
|
+
if (after.dirty || after.headSha !== plan.expectedCommit || after.branch !== plan.expectedBranch) fail('checkout-changed');
|
|
168
|
+
return Object.freeze({ status: result.status === 'success' ? 'ready' : 'failed', result });
|
|
169
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, unlink } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
|
|
5
|
+
import { validateEvent } from '../config/validate.js';
|
|
6
|
+
import { acquireLock, LockBusyError } from './lock.js';
|
|
7
|
+
import { assertResolvedStatePaths, verifyResolvedStatePaths } from './paths.js';
|
|
8
|
+
import { redactSecrets } from './redact.js';
|
|
9
|
+
|
|
10
|
+
const MAX_LEDGER_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const MAX_EVENT_BYTES = 64 * 1024;
|
|
12
|
+
const LOCK_WAIT_MS = 2_000;
|
|
13
|
+
const LOCK_RETRY_MS = 5;
|
|
14
|
+
const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
15
|
+
const DIRECTORY = constants.O_DIRECTORY ?? 0;
|
|
16
|
+
|
|
17
|
+
function sameIdentity(left, right) {
|
|
18
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function privateRegularFile(metadata) {
|
|
22
|
+
return metadata.isFile()
|
|
23
|
+
&& !metadata.isSymbolicLink()
|
|
24
|
+
&& metadata.nlink === 1
|
|
25
|
+
&& (metadata.mode & 0o777) === 0o600;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class StateVersionConflictError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super('state version conflict');
|
|
31
|
+
this.name = 'StateVersionConflictError';
|
|
32
|
+
this.code = 'ERR_STATE_VERSION_CONFLICT';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class StateDurabilityError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('Private state durability is unsupported on this filesystem');
|
|
39
|
+
this.name = 'StateDurabilityError';
|
|
40
|
+
this.code = 'ERR_STATE_DURABILITY_UNSUPPORTED';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function existingMetadata(path, label) {
|
|
45
|
+
try {
|
|
46
|
+
const metadata = await lstat(path);
|
|
47
|
+
if (!privateRegularFile(metadata)) throw new Error(`Unsafe ${label}`);
|
|
48
|
+
return metadata;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === 'ENOENT') return null;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readBoundedUtf8(handle, maximumBytes) {
|
|
56
|
+
const buffer = Buffer.alloc(maximumBytes + 1);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
while (offset < buffer.length) {
|
|
59
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
|
|
60
|
+
if (bytesRead === 0) break;
|
|
61
|
+
offset += bytesRead;
|
|
62
|
+
}
|
|
63
|
+
if (offset > maximumBytes) throw new Error('Invalid event ledger');
|
|
64
|
+
try {
|
|
65
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, offset));
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error('Invalid event ledger');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseLedger(source) {
|
|
72
|
+
if (!source || !source.endsWith('\n')) throw new Error('Invalid event ledger');
|
|
73
|
+
const lines = source.slice(0, -1).split('\n');
|
|
74
|
+
const events = [];
|
|
75
|
+
const eventIds = new Set();
|
|
76
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
77
|
+
if (!lines[index] || Buffer.byteLength(lines[index]) > MAX_EVENT_BYTES) throw new Error('Invalid event ledger');
|
|
78
|
+
let event;
|
|
79
|
+
try {
|
|
80
|
+
event = JSON.parse(lines[index]);
|
|
81
|
+
validateEvent(event);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error('Invalid event ledger');
|
|
84
|
+
}
|
|
85
|
+
if (event.sequence !== index + 1 || eventIds.has(event.eventId)) throw new Error('Invalid event ledger');
|
|
86
|
+
eventIds.add(event.eventId);
|
|
87
|
+
events.push(event);
|
|
88
|
+
}
|
|
89
|
+
return events;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readOpenedLedger(handle, opened) {
|
|
93
|
+
if (opened.size <= 0 || opened.size > MAX_LEDGER_BYTES) throw new Error('Invalid event ledger');
|
|
94
|
+
return parseLedger(await readBoundedUtf8(handle, MAX_LEDGER_BYTES));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function openExistingLedger(path, before, flags = constants.O_RDONLY) {
|
|
98
|
+
if (!before || before.size <= 0 || before.size > MAX_LEDGER_BYTES) throw new Error('Invalid event ledger');
|
|
99
|
+
const handle = await open(path, flags | NOFOLLOW);
|
|
100
|
+
try {
|
|
101
|
+
const opened = await handle.stat();
|
|
102
|
+
if (!sameIdentity(before, opened) || !privateRegularFile(opened) || opened.size !== before.size) {
|
|
103
|
+
throw new Error('Unsafe event ledger');
|
|
104
|
+
}
|
|
105
|
+
return { handle, opened };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await handle.close();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readLedger(path) {
|
|
113
|
+
const before = await existingMetadata(path, 'event ledger');
|
|
114
|
+
if (!before) return [];
|
|
115
|
+
const { handle, opened } = await openExistingLedger(path, before);
|
|
116
|
+
try {
|
|
117
|
+
const events = await readOpenedLedger(handle, opened);
|
|
118
|
+
const after = await lstat(path);
|
|
119
|
+
if (!privateRegularFile(after) || !sameIdentity(opened, after) || after.size !== opened.size) {
|
|
120
|
+
throw new Error('Event ledger identity changed');
|
|
121
|
+
}
|
|
122
|
+
return events;
|
|
123
|
+
} finally {
|
|
124
|
+
await handle.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function acquireWithRetry(path, options) {
|
|
129
|
+
const lockWaitMs = options.lockWaitMs ?? LOCK_WAIT_MS;
|
|
130
|
+
if (!Number.isSafeInteger(lockWaitMs) || lockWaitMs < 0 || lockWaitMs > 60_000) {
|
|
131
|
+
throw new TypeError('Invalid state lock wait');
|
|
132
|
+
}
|
|
133
|
+
const deadline = Date.now() + lockWaitMs;
|
|
134
|
+
while (true) {
|
|
135
|
+
try {
|
|
136
|
+
return await acquireLock(path, options.lockOptions);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!(error instanceof LockBusyError) || Date.now() >= deadline) throw error;
|
|
139
|
+
await delay(LOCK_RETRY_MS);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function createLedger(path) {
|
|
145
|
+
let handle;
|
|
146
|
+
try {
|
|
147
|
+
handle = await open(
|
|
148
|
+
path,
|
|
149
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_APPEND | constants.O_RDWR | NOFOLLOW,
|
|
150
|
+
0o600,
|
|
151
|
+
);
|
|
152
|
+
await handle.chmod(0o600);
|
|
153
|
+
const opened = await handle.stat();
|
|
154
|
+
if (!privateRegularFile(opened) || opened.size !== 0) throw new Error('Unsafe event ledger');
|
|
155
|
+
return { handle, opened };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await handle?.close();
|
|
158
|
+
throw error.code === 'EEXIST' ? new Error('Event ledger identity changed') : error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function removeCreatedLedger(path, expected) {
|
|
163
|
+
try {
|
|
164
|
+
const current = await lstat(path);
|
|
165
|
+
if (privateRegularFile(current) && sameIdentity(current, expected)) await unlink(path);
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function openPrivateDirectory(path) {
|
|
170
|
+
const before = await lstat(path);
|
|
171
|
+
if (!before.isDirectory() || before.isSymbolicLink() || (before.mode & 0o777) !== 0o700) {
|
|
172
|
+
throw new Error('Private state path identity changed');
|
|
173
|
+
}
|
|
174
|
+
const handle = await open(path, constants.O_RDONLY | DIRECTORY | NOFOLLOW);
|
|
175
|
+
try {
|
|
176
|
+
const opened = await handle.stat();
|
|
177
|
+
if (!opened.isDirectory() || !sameIdentity(before, opened)) throw new Error('Private state path identity changed');
|
|
178
|
+
return handle;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
await handle.close();
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function appendLine(handle, opened, path, line) {
|
|
186
|
+
const lineBytes = Buffer.byteLength(line);
|
|
187
|
+
if (opened.size + lineBytes > MAX_LEDGER_BYTES) throw new Error('Event ledger exceeds storage limit');
|
|
188
|
+
await handle.writeFile(line, { encoding: 'utf8' });
|
|
189
|
+
await handle.sync();
|
|
190
|
+
const descriptorAfter = await handle.stat();
|
|
191
|
+
const after = await lstat(path);
|
|
192
|
+
if (
|
|
193
|
+
!privateRegularFile(descriptorAfter)
|
|
194
|
+
|| !privateRegularFile(after)
|
|
195
|
+
|| !sameIdentity(opened, descriptorAfter)
|
|
196
|
+
|| !sameIdentity(descriptorAfter, after)
|
|
197
|
+
|| descriptorAfter.size !== opened.size + lineBytes
|
|
198
|
+
|| after.size !== descriptorAfter.size
|
|
199
|
+
) {
|
|
200
|
+
throw new Error('Event ledger identity changed');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function closeHandles(handles) {
|
|
205
|
+
const results = await Promise.allSettled(handles.filter(Boolean).map(handle => handle.close()));
|
|
206
|
+
const failed = results.find(result => result.status === 'rejected');
|
|
207
|
+
if (failed) throw failed.reason;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function captureLockOptions(lockOptions) {
|
|
211
|
+
if (lockOptions === undefined) return undefined;
|
|
212
|
+
if (!lockOptions || typeof lockOptions !== 'object') throw new TypeError('Invalid state lock options');
|
|
213
|
+
return Object.freeze({
|
|
214
|
+
staleAfterMs: lockOptions.staleAfterMs,
|
|
215
|
+
now: lockOptions.now,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function captureStoreOptions(options) {
|
|
220
|
+
const syncDirectory = options.syncDirectory ?? (async handle => {
|
|
221
|
+
try {
|
|
222
|
+
await handle.sync();
|
|
223
|
+
} catch {
|
|
224
|
+
throw new StateDurabilityError();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (typeof syncDirectory !== 'function') throw new TypeError('Invalid directory sync function');
|
|
228
|
+
return Object.freeze({
|
|
229
|
+
lockWaitMs: options.lockWaitMs,
|
|
230
|
+
lockOptions: captureLockOptions(options.lockOptions),
|
|
231
|
+
secretKeys: options.secretKeys,
|
|
232
|
+
environment: options.environment,
|
|
233
|
+
syncDirectory,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertPaths(paths) {
|
|
238
|
+
assertResolvedStatePaths(paths);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createEventStore(paths, options = {}) {
|
|
242
|
+
assertPaths(paths);
|
|
243
|
+
const storeOptions = captureStoreOptions(options);
|
|
244
|
+
|
|
245
|
+
async function withLock(operation) {
|
|
246
|
+
await verifyResolvedStatePaths(paths);
|
|
247
|
+
const lock = await acquireWithRetry(paths.lockPath, storeOptions);
|
|
248
|
+
try {
|
|
249
|
+
await verifyResolvedStatePaths(paths);
|
|
250
|
+
return await operation();
|
|
251
|
+
} finally {
|
|
252
|
+
await lock.release();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return Object.freeze({
|
|
257
|
+
async read() {
|
|
258
|
+
return withLock(() => readLedger(paths.eventsPath));
|
|
259
|
+
},
|
|
260
|
+
async version() {
|
|
261
|
+
return withLock(async () => (await readLedger(paths.eventsPath)).length);
|
|
262
|
+
},
|
|
263
|
+
async append(input, appendOptions = {}) {
|
|
264
|
+
const expectedVersion = appendOptions.expectedVersion;
|
|
265
|
+
const prepared = redactSecrets(input, {
|
|
266
|
+
secretKeys: appendOptions.secretKeys ?? storeOptions.secretKeys,
|
|
267
|
+
environment: appendOptions.environment ?? storeOptions.environment ?? process.env,
|
|
268
|
+
});
|
|
269
|
+
if (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0) {
|
|
270
|
+
throw new TypeError('Append requires a non-negative expectedVersion');
|
|
271
|
+
}
|
|
272
|
+
validateEvent({ ...prepared, sequence: prepared?.sequence ?? 1 });
|
|
273
|
+
return withLock(async () => {
|
|
274
|
+
const directoryHandle = await openPrivateDirectory(paths.instanceDir);
|
|
275
|
+
let ledgerHandle;
|
|
276
|
+
try {
|
|
277
|
+
const before = await existingMetadata(paths.eventsPath, 'event ledger');
|
|
278
|
+
let opened;
|
|
279
|
+
let events;
|
|
280
|
+
let created = false;
|
|
281
|
+
if (before) {
|
|
282
|
+
({ handle: ledgerHandle, opened } = await openExistingLedger(
|
|
283
|
+
paths.eventsPath,
|
|
284
|
+
before,
|
|
285
|
+
constants.O_APPEND | constants.O_RDWR,
|
|
286
|
+
));
|
|
287
|
+
await verifyResolvedStatePaths(paths);
|
|
288
|
+
events = await readOpenedLedger(ledgerHandle, opened);
|
|
289
|
+
} else {
|
|
290
|
+
events = [];
|
|
291
|
+
}
|
|
292
|
+
const currentVersion = events.length;
|
|
293
|
+
if (expectedVersion !== currentVersion) throw new StateVersionConflictError();
|
|
294
|
+
const nextSequence = currentVersion + 1;
|
|
295
|
+
if (prepared?.sequence !== undefined && prepared.sequence !== nextSequence) throw new Error('Event sequence conflict');
|
|
296
|
+
const persisted = { ...prepared, sequence: nextSequence };
|
|
297
|
+
validateEvent(persisted);
|
|
298
|
+
if (events.some(event => event.eventId === persisted.eventId)) throw new Error('Duplicate event identifier');
|
|
299
|
+
const line = `${JSON.stringify(persisted)}\n`;
|
|
300
|
+
if (Buffer.byteLength(line) > MAX_EVENT_BYTES) throw new Error('Event exceeds storage limit');
|
|
301
|
+
if (!ledgerHandle) {
|
|
302
|
+
({ handle: ledgerHandle, opened } = await createLedger(paths.eventsPath));
|
|
303
|
+
created = true;
|
|
304
|
+
try {
|
|
305
|
+
await verifyResolvedStatePaths(paths);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const createdHandle = ledgerHandle;
|
|
308
|
+
ledgerHandle = null;
|
|
309
|
+
try {
|
|
310
|
+
await createdHandle.close();
|
|
311
|
+
} finally {
|
|
312
|
+
await removeCreatedLedger(paths.eventsPath, opened);
|
|
313
|
+
}
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await appendLine(ledgerHandle, opened, paths.eventsPath, line);
|
|
318
|
+
if (created) {
|
|
319
|
+
try {
|
|
320
|
+
await storeOptions.syncDirectory(directoryHandle);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof StateDurabilityError) throw error;
|
|
323
|
+
throw new StateDurabilityError();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return persisted;
|
|
327
|
+
} finally {
|
|
328
|
+
await closeHandles([ledgerHandle, directoryHandle]);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|