@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,195 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { access, lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { types } from 'node:util';
|
|
5
|
+
import metadata from '../../package.json' with { type: 'json' };
|
|
6
|
+
import { diagnoseDoctor } from '../commands/doctor.js';
|
|
7
|
+
import { discoverHarnesses } from '../runtime/harness-discovery.js';
|
|
8
|
+
import { CliError } from '../cli/output.js';
|
|
9
|
+
const TOOLS = ['node','npm','pnpm','yarn','bun','git'];
|
|
10
|
+
const PLATFORM = new Set(['aix','darwin','freebsd','linux','openbsd','sunos','win32']);
|
|
11
|
+
const INTEGRATION_READINESS = ['disabled','out-of-scope','unsupported-transport','host-unavailable','authentication-unavailable','unsupported-tools','host-observed','credentials-present'];
|
|
12
|
+
const ARCH = new Set(['arm','arm64','ia32','loong64','mips','mipsel','ppc','ppc64','riscv64','s390','s390x','x64']);
|
|
13
|
+
const BAD = () => { throw new Error('Invalid diagnostic data'); };
|
|
14
|
+
function snapshot(input) {
|
|
15
|
+
let nodes=0,bytes=0;
|
|
16
|
+
function copy(value,depth=0) {
|
|
17
|
+
if (++nodes>20000 || depth>20 || types.isProxy(value)) BAD();
|
|
18
|
+
if (value===null || typeof value==='boolean')return value;
|
|
19
|
+
if (typeof value==='string') { bytes+=Buffer.byteLength(value); if(bytes>1024*1024 || value.length>65536)BAD(); return value; }
|
|
20
|
+
if (typeof value==='number') {if(!Number.isSafeInteger(value))BAD();return value;}
|
|
21
|
+
if (!value || typeof value!=='object')BAD();
|
|
22
|
+
const array=Array.isArray(value);
|
|
23
|
+
if(![Object.prototype,null,...(array?[Array.prototype]:[])].includes(Object.getPrototypeOf(value)))BAD();
|
|
24
|
+
const keys=Reflect.ownKeys(value);if(keys.length>2048)BAD();
|
|
25
|
+
const result=array?[]:{};
|
|
26
|
+
for(const key of keys) {
|
|
27
|
+
if(array&&key==='length')continue;
|
|
28
|
+
const descriptor=Object.getOwnPropertyDescriptor(value,key);
|
|
29
|
+
if(typeof key!=='string'||key==='__proto__'||!descriptor?.enumerable||!Object.hasOwn(descriptor,'value'))BAD();
|
|
30
|
+
if(array&&(!/^(?:0|[1-9][0-9]*)$/.test(key)||Number(key)>=value.length))BAD();
|
|
31
|
+
result[key]=copy(descriptor.value,depth+1);
|
|
32
|
+
}
|
|
33
|
+
if(array&&(result.length!==value.length||Object.keys(result).length!==value.length))BAD();
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
return copy(input);
|
|
37
|
+
}
|
|
38
|
+
function environmentSnapshot(environment) {
|
|
39
|
+
if(!environment||typeof environment!=='object'||Array.isArray(environment)||types.isProxy(environment))BAD();
|
|
40
|
+
const values=Object.create(null),keys=Reflect.ownKeys(environment);if(keys.length>1024)BAD();
|
|
41
|
+
for(const key of keys) {
|
|
42
|
+
const descriptor=Object.getOwnPropertyDescriptor(environment,key);
|
|
43
|
+
if(typeof key!=='string'||!descriptor?.enumerable||!Object.hasOwn(descriptor,'value')||typeof descriptor.value!=='string'||descriptor.value.length>65536)BAD();
|
|
44
|
+
values[key]=descriptor.value;
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze(values);
|
|
47
|
+
}
|
|
48
|
+
function version(value, forbidden=[]) {
|
|
49
|
+
if(typeof value!=='string'||value.length>1024)return null;
|
|
50
|
+
const match=/^(?:v|codex-cli |git version )?(\d{1,6}\.\d{1,6}\.\d{1,6})(?:[-+][A-Za-z0-9.-]+)?(?: \(Claude Code\))?$/.exec(value);
|
|
51
|
+
const core=match?.[1]??null;
|
|
52
|
+
return core&&forbidden.some(secret=>secret&&secret.includes(core))?null:core;
|
|
53
|
+
}
|
|
54
|
+
const count=(items,predicate)=>Array.isArray(items)?Math.min(2048,items.filter(predicate).length):0;
|
|
55
|
+
function normalDoctor(input,environment) {
|
|
56
|
+
const report=snapshot(input);
|
|
57
|
+
if(!report||typeof report!=='object'||Array.isArray(report)||!['pass','warn','fail'].includes(report.status)
|
|
58
|
+
||!report.checks||!['pass','fail'].includes(report.checks.configuration?.status))BAD();
|
|
59
|
+
const checks=report.checks;
|
|
60
|
+
for(const key of ['credentials','providers','integrations'])if(checks[key]!==undefined&&!Array.isArray(checks[key]))BAD();
|
|
61
|
+
const forbidden=(Array.isArray(checks.credentials)?checks.credentials:[]).map(item=>typeof item.name==='string'?environment[item.name]:'').filter(Boolean);
|
|
62
|
+
return {
|
|
63
|
+
configuration:{status:checks.configuration.status==='pass'?'valid':'missing-or-invalid'},
|
|
64
|
+
readiness:{status:report.status,
|
|
65
|
+
credentials:{required:count(checks.credentials,item=>item.required===true),missing:count(checks.credentials,item=>item.required===true&&item.present===false)},
|
|
66
|
+
providers:{configured:count(checks.providers,()=>true),unavailable:count(checks.providers,item=>['unavailable','timeout','error'].includes(item.connectivity)),disabled:count(checks.providers,item=>item.readiness==='disabled')},
|
|
67
|
+
commands:{status:checks.commands?.ready===true?'ready':checks.commands?.ready===false?'not-ready':'unknown',unready:count(checks.commands?.steps,item=>item.status!=='ready')},
|
|
68
|
+
integrations:{configured:Array.isArray(checks.integrations)?Math.min(checks.integrations.length,2048):0,
|
|
69
|
+
readiness:Object.fromEntries([...INTEGRATION_READINESS,'unknown'].map(state=>[state,count(checks.integrations,item=>state==='unknown'?!INTEGRATION_READINESS.includes(item.readiness):item.readiness===state)]))}},
|
|
70
|
+
tools:TOOLS.filter(name=>Object.hasOwn(checks.tools??{},name)).map(name=>{
|
|
71
|
+
const tool=checks.tools[name];if(!tool||typeof tool!=='object'||Array.isArray(tool)
|
|
72
|
+
||(tool.present!==undefined&&typeof tool.present!=='boolean')||(tool.supported!==undefined&&typeof tool.supported!=='boolean')
|
|
73
|
+
||(tool.version!==undefined&&tool.version!==null&&typeof tool.version!=='string'))BAD();
|
|
74
|
+
return {name,present:tool.present===true,supported:tool.supported===true,version:version(tool.version,forbidden)};
|
|
75
|
+
}),
|
|
76
|
+
forbidden,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function normalHarnesses(input,forbidden) {
|
|
80
|
+
const rows=snapshot(input);if(!Array.isArray(rows)||rows.length!==2)BAD();
|
|
81
|
+
const seen=new Set();
|
|
82
|
+
return rows.map(row=>{
|
|
83
|
+
if(!row||!['claude','codex'].includes(row.kind)||seen.has(row.kind))BAD();seen.add(row.kind);
|
|
84
|
+
const present=typeof row.executable==='string'&&row.executable.length>0;
|
|
85
|
+
const reasons=new Set(['not-installed','capability-probe-failed','interpreter-required','interpreter-unneeded','interpreter-unavailable','interpreter-incompatible','executable-unsafe']);
|
|
86
|
+
const reason=present?'available':typeof row.reason==='string'&&row.reason.startsWith('missing-options:')?'missing-options':reasons.has(row.reason)?row.reason:'probe-failed';
|
|
87
|
+
return {kind:row.kind,present,supported:present,version:version(row.version,forbidden),reason};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function resolveSupportProject(cwd,explicit) {
|
|
91
|
+
if(typeof cwd!=='string'||!isAbsolute(cwd)||/[\0\r\n]/.test(cwd)
|
|
92
|
+
||(explicit!==undefined&&(typeof explicit!=='string'||!explicit||/[\0\r\n]/.test(explicit))))throw new CliError('Project directory is invalid.','INVALID_INPUT');
|
|
93
|
+
let selected;
|
|
94
|
+
try {selected=await realpath(resolve(cwd,explicit??'.'));if(!(await lstat(selected)).isDirectory())throw new Error();}
|
|
95
|
+
catch {throw new CliError('Project directory is unavailable.','INVALID_INPUT');}
|
|
96
|
+
if(explicit!==undefined)return selected;
|
|
97
|
+
let current=selected;
|
|
98
|
+
for(let depth=0;depth<64;depth++) {
|
|
99
|
+
for(const marker of ['.rivet','.git']) {
|
|
100
|
+
try {await lstat(join(current,marker));return current;}catch(error){if(error.code!=='ENOENT')throw new CliError('Project discovery is unavailable.','INVALID_INPUT');}
|
|
101
|
+
}
|
|
102
|
+
const parent=dirname(current);if(parent===current)break;current=parent;
|
|
103
|
+
}
|
|
104
|
+
return selected;
|
|
105
|
+
}
|
|
106
|
+
function versionRunner(signal,environment,projectRoot) {
|
|
107
|
+
const outside = path => { const rel=relative(projectRoot,path);return rel==='..'||rel.startsWith(`..${sep}`)||isAbsolute(rel); };
|
|
108
|
+
let directoriesPromise;
|
|
109
|
+
const directories = () => directoriesPromise ??= (async()=>{
|
|
110
|
+
const choices=[...(environment.PATH??'').split(delimiter).slice(0,128),'/opt/homebrew/bin','/usr/local/bin','/usr/bin','/bin'];
|
|
111
|
+
const safe=[];
|
|
112
|
+
for(const path of choices) {
|
|
113
|
+
if(signal.aborted)break;
|
|
114
|
+
if(!isAbsolute(path)||/[\0\r\n]/.test(path))continue;
|
|
115
|
+
try {const canonical=await realpath(path);if(outside(canonical)&&(await lstat(canonical)).isDirectory()&&!safe.includes(canonical))safe.push(canonical);}catch{}
|
|
116
|
+
}
|
|
117
|
+
return safe;
|
|
118
|
+
})();
|
|
119
|
+
return async (command,args,options={})=>{
|
|
120
|
+
const unavailable={code:1,stdout:'',stderr:'',timedOut:false};
|
|
121
|
+
if(signal.aborted||!TOOLS.includes(command)||!Array.isArray(args)||args.length!==1||args[0]!=='--version')return unavailable;
|
|
122
|
+
const paths=await directories();let executable;
|
|
123
|
+
for(const directory of paths) {
|
|
124
|
+
if(signal.aborted)return unavailable;
|
|
125
|
+
try {
|
|
126
|
+
const candidate=await realpath(join(directory,command));
|
|
127
|
+
if(!outside(candidate)||!(await lstat(candidate)).isFile())continue;
|
|
128
|
+
await access(candidate,1);executable=candidate;break;
|
|
129
|
+
}catch{}
|
|
130
|
+
}
|
|
131
|
+
if(!executable||signal.aborted)return unavailable;
|
|
132
|
+
const probeEnvironment=Object.fromEntries(['LANG','LC_ALL','TZ','TERM','HOME','USER','LOGNAME','TMPDIR','SystemRoot','WINDIR']
|
|
133
|
+
.filter(key=>environment[key]!==undefined).map(key=>[key,environment[key]]));
|
|
134
|
+
probeEnvironment.PATH=paths.join(delimiter);
|
|
135
|
+
// A globally installed Yarn/Corepack shim can still redirect from a project
|
|
136
|
+
// packageManager/yarnPath setting. Probe from the system root instead, disable
|
|
137
|
+
// project redirection and downloads, and never permit automatic project pins.
|
|
138
|
+
Object.assign(probeEnvironment, {
|
|
139
|
+
YARN_IGNORE_PATH: '1', COREPACK_ENABLE_PROJECT_SPEC: '0',
|
|
140
|
+
COREPACK_ENABLE_NETWORK: '0', COREPACK_ENABLE_AUTO_PIN: '0',
|
|
141
|
+
COREPACK_DEFAULT_TO_LATEST: '0',
|
|
142
|
+
});
|
|
143
|
+
const probeCwd=parse(process.execPath).root;
|
|
144
|
+
return new Promise(resolvePromise=>{
|
|
145
|
+
execFile(executable,args,{cwd:probeCwd,env:probeEnvironment,shell:false,timeout:3000,maxBuffer:16384,signal,killSignal:'SIGKILL'},(error,stdout)=>{
|
|
146
|
+
resolvePromise({code:error?1:0,stdout:typeof stdout==='string'?stdout:'',stderr:'',timedOut:error?.killed===true});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function collectSupportBundle(projectRoot,options={}) {
|
|
152
|
+
const controller=new AbortController(),abort=()=>controller.abort();
|
|
153
|
+
let timer,timedOut=false,environment,forbidden=[];
|
|
154
|
+
const bundle={schemaVersion:1,versions:{rivet:version(metadata.version),node:version(process.version),platform:PLATFORM.has(process.platform)?process.platform:'unknown',architecture:ARCH.has(process.arch)?process.arch:'unknown'},
|
|
155
|
+
collection:{status:'complete',errors:[]},configuration:{status:'unknown'},readiness:{status:'unknown'},tools:[],harnesses:{status:'not-requested',results:[]}};
|
|
156
|
+
const incomplete=reason=>{bundle.collection.status='incomplete';if(!bundle.collection.errors.includes(reason))bundle.collection.errors.push(reason)};
|
|
157
|
+
const bounded=operation=>new Promise((resolvePromise,reject)=>{
|
|
158
|
+
if(controller.signal.aborted){reject(new Error());return;}
|
|
159
|
+
const cancel=()=>reject(new Error());controller.signal.addEventListener('abort',cancel,{once:true});
|
|
160
|
+
Promise.resolve().then(()=>{if(controller.signal.aborted)throw new Error();return operation()}).then(resolvePromise,reject)
|
|
161
|
+
.finally(()=>controller.signal.removeEventListener('abort',cancel));
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
environment=environmentSnapshot(options.environment??process.env);
|
|
165
|
+
if(options.signal!==undefined){
|
|
166
|
+
const getter=Object.getOwnPropertyDescriptor(AbortSignal.prototype,'aborted').get;
|
|
167
|
+
if(Reflect.apply(getter,options.signal,[]))abort();
|
|
168
|
+
Reflect.apply(EventTarget.prototype.addEventListener,options.signal,['abort',abort,{once:true}]);
|
|
169
|
+
}
|
|
170
|
+
const timeoutMs=options.timeoutMs??15000;
|
|
171
|
+
if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>15000)BAD();
|
|
172
|
+
timer=setTimeout(()=>{timedOut=true;abort()},timeoutMs);
|
|
173
|
+
let unsafe=false;
|
|
174
|
+
try {const marker=await lstat(join(projectRoot,'.rivet'));unsafe=marker.isSymbolicLink()||!marker.isDirectory();}catch(error){if(error.code!=='ENOENT')unsafe=true;}
|
|
175
|
+
if(unsafe)bundle.configuration.status='unsafe';
|
|
176
|
+
else try {
|
|
177
|
+
const diagnostic=await bounded(()=>(options.diagnose??diagnoseDoctor)(projectRoot,{env:environment,runner:versionRunner(controller.signal,environment,projectRoot),providerProbe:undefined}));
|
|
178
|
+
const normalized=normalDoctor(diagnostic,environment);forbidden=normalized.forbidden;delete normalized.forbidden;Object.assign(bundle,normalized);
|
|
179
|
+
bundle.versions.rivet=version(metadata.version,forbidden);bundle.versions.node=version(process.version,forbidden);
|
|
180
|
+
}catch {incomplete(controller.signal.aborted?(timedOut?'timeout':'cancelled'):'doctor-failed')}
|
|
181
|
+
if(options.probeHarnesses===true){
|
|
182
|
+
bundle.harnesses.status='failed';
|
|
183
|
+
try {
|
|
184
|
+
const rows=await bounded(()=>(options.discoverHarnesses??discoverHarnesses)({env:environment,projectRoot,signal:controller.signal}));
|
|
185
|
+
bundle.harnesses={status:'complete',results:normalHarnesses(rows,forbidden)};
|
|
186
|
+
}catch {incomplete(controller.signal.aborted?(timedOut?'timeout':'cancelled'):'harness-probe-failed')}
|
|
187
|
+
}
|
|
188
|
+
if(controller.signal.aborted)incomplete(timedOut?'timeout':'cancelled');
|
|
189
|
+
}catch {incomplete('collector-failed')}
|
|
190
|
+
finally {
|
|
191
|
+
clearTimeout(timer);controller.abort();
|
|
192
|
+
if(options.signal!==undefined)try{Reflect.apply(EventTarget.prototype.removeEventListener,options.signal,['abort',abort])}catch{}
|
|
193
|
+
}
|
|
194
|
+
return Object.freeze(bundle);
|
|
195
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { chmod, lstat, mkdtemp, open, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
6
|
+
import metadata from '../../package.json' with { type: 'json' };
|
|
7
|
+
import { EXIT_CODES } from '../cli/output.js';
|
|
8
|
+
import { collectSupportBundle } from './bundle.js';
|
|
9
|
+
|
|
10
|
+
const COMMANDS = new Set(['doctor','support','delivery','evidence','feature','goals','init','install','models','integrations','repositories','orchestrate','preflight','protocols','run','status','setup','uninstall','verify','work','task','worktrees']);
|
|
11
|
+
const SUBCOMMANDS = new Set(['status','start','approve','resume','deps','recover','propose','watch','prepare','publish','review','merge','deploy','inspect','list','check','discover','configure','intake','add','remove','show','validate','plan','activate','cancel','stop','next','complete','submit','verify','record','sync','doctor','create','clean']);
|
|
12
|
+
const PLATFORMS = new Set(['aix','darwin','freebsd','linux','openbsd','sunos','win32']);
|
|
13
|
+
const ARCHITECTURES = new Set(['arm','arm64','ia32','loong64','mips','mipsel','ppc','ppc64','riscv64','s390','s390x','x64']);
|
|
14
|
+
// Deliberately closed: arbitrary provider/OS codes can contain private input.
|
|
15
|
+
// These values correspond to fixed error constructors/reasons in Rivet source.
|
|
16
|
+
const CAUSE_CODES = new Set([
|
|
17
|
+
'ERR_HOST_RUN_BUSY', 'ERR_HOST_RUN_STALE_LOCK', 'ERR_HOST_RUN_RECOVERY_STATE',
|
|
18
|
+
'ERR_FEATURE_RUN_VERSION_CONFLICT', 'ERR_INVALID_FEATURE_RUN', 'ERR_INVALID_FEATURE_PLAN',
|
|
19
|
+
'ERR_INVALID_FEATURE_DECOMPOSITION', 'ERR_FEATURE_RUNTIME_BRIDGE', 'ERR_VERIFICATION_REPORT_VERSION_CONFLICT',
|
|
20
|
+
'ERR_FEATURE_WORKFLOW_CONFIGURATION', 'ERR_FEATURE_WORKFLOW_PROPOSAL_MISMATCH',
|
|
21
|
+
'ERR_FEATURE_WORKFLOW_STATE_CONFLICT', 'ERR_FEATURE_WORKFLOW_REPOSITORY', 'ERR_FEATURE_WORKFLOW_RUN_MISSING',
|
|
22
|
+
'ERR_HOST_EXECUTION_VERIFICATION_FAILED', 'ERR_HOST_EXECUTION_STATE_CONFLICT', 'ERR_HOST_EXECUTION_REPOSITORY',
|
|
23
|
+
'ERR_LOCAL_APPROVAL_INVALID', 'ERR_LOCAL_APPROVAL_CONFIGURATION', 'ERR_LOCAL_APPROVAL_INTERRUPTED', 'ERR_LOCAL_APPROVAL_CANCELLED',
|
|
24
|
+
'ERR_GIT_NON_FAST_FORWARD', 'ERR_GIT_REPOSITORY_CHANGED', 'ERR_GIT_REPOSITORY_PATH_UNSAFE',
|
|
25
|
+
'ERR_GIT_UNSAFE_EXECUTABLE', 'ERR_GIT_GIT_OUTPUT_INVALID', 'ERR_GIT_REVIEW_DIFF_UNAVAILABLE',
|
|
26
|
+
'ERR_AGENT_TIMEOUT', 'ERR_AGENT_PROVIDER_UNAVAILABLE', 'ERR_AGENT_ABORTED', 'ERR_AGENT_OUTPUT_INVALID',
|
|
27
|
+
'ERR_AGENT_EXECUTABLE_UNSAFE', 'ERR_AGENT_CWD_UNSAFE', 'ERR_AGENT_TEMPLATE_INVALID',
|
|
28
|
+
'ERR_APPLICATION_CONFIGURATION', 'ERR_STATE_VERSION_CONFLICT', 'ERR_RUNTIME_APPROVAL_REQUIRED',
|
|
29
|
+
'ERR_RUNTIME_NOT_ACTIVATED', 'ERR_RUNTIME_VERSION_CONFLICT', 'ERR_RUNTIME_CLIENT_OUTPUT',
|
|
30
|
+
]);
|
|
31
|
+
const ISSUE_URL = 'https://github.com/Agilno-Tech/rivet/issues/new';
|
|
32
|
+
let fingerprint;
|
|
33
|
+
async function runtimeFingerprint() {
|
|
34
|
+
// Only installed Rivet source files, never the user's repository or runtime state.
|
|
35
|
+
return fingerprint ??= Promise.all(['../../package.json','../cli/main.js','../cli/parse-args.js'].map(path=>readFile(new URL(path,import.meta.url))))
|
|
36
|
+
.then(files=>{const hash=createHash('sha256');for(const file of files)hash.update(String(file.length)).update(':').update(file);return hash.digest('hex');})
|
|
37
|
+
.catch(()=>null);
|
|
38
|
+
}
|
|
39
|
+
function version(value) { return typeof value==='string' && /^v?\d{1,6}\.\d{1,6}\.\d{1,6}(?:-[A-Za-z0-9.-]{1,64})?$/.test(value) ? value.replace(/^v/,'') : 'unknown'; }
|
|
40
|
+
async function safeParent(temporaryDirectory) {
|
|
41
|
+
const selected=temporaryDirectory??tmpdir();
|
|
42
|
+
if(typeof selected!=='string'||!isAbsolute(selected)||/[\0\r\n]/.test(selected))throw new Error();
|
|
43
|
+
const initial=await lstat(selected);
|
|
44
|
+
if(!initial.isDirectory()||initial.isSymbolicLink())throw new Error();
|
|
45
|
+
const parent=await realpath(selected),uid=process.getuid?.();
|
|
46
|
+
for(let current=parent;;current=dirname(current)) {
|
|
47
|
+
const stat=await lstat(current);
|
|
48
|
+
if(!stat.isDirectory()||stat.isSymbolicLink())throw new Error();
|
|
49
|
+
if(uid!==undefined && stat.uid!==uid && stat.uid!==0)throw new Error();
|
|
50
|
+
// A root-owned sticky system temp directory is safe for a private mkdtemp child.
|
|
51
|
+
if(uid!==undefined && (stat.mode&0o022)!==0 && !(stat.uid===0 && (stat.mode&0o1000)!==0))throw new Error();
|
|
52
|
+
if(dirname(current)===current)break;
|
|
53
|
+
}
|
|
54
|
+
return parent;
|
|
55
|
+
}
|
|
56
|
+
async function savePrivate(report,{temporaryDirectory}={}) {
|
|
57
|
+
const serialized=`${JSON.stringify(report,null,2)}\n`;
|
|
58
|
+
if(Buffer.byteLength(serialized)>64*1024)throw new Error();
|
|
59
|
+
const parent=await safeParent(temporaryDirectory);
|
|
60
|
+
const directory=await mkdtemp(join(parent,'rivet-support-'));
|
|
61
|
+
await chmod(directory,0o700);
|
|
62
|
+
const stat=await lstat(directory);
|
|
63
|
+
if(!stat.isDirectory()||stat.isSymbolicLink()||(process.getuid && stat.uid!==process.getuid())||(stat.mode&0o777)!==0o700)throw new Error();
|
|
64
|
+
const path=join(directory,'report.json');
|
|
65
|
+
const file=await open(path,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|(constants.O_NOFOLLOW??0),0o600);
|
|
66
|
+
try {
|
|
67
|
+
const opened=await file.stat();
|
|
68
|
+
if(!opened.isFile()||opened.nlink!==1||(opened.mode&0o777)!==0o600||(process.getuid && opened.uid!==process.getuid()))throw new Error();
|
|
69
|
+
await file.writeFile(serialized,'utf8');await file.sync();
|
|
70
|
+
} finally { await file.close(); }
|
|
71
|
+
return {path,report};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Best effort: accepts only fixed public categories; never records raw error data. */
|
|
75
|
+
export async function saveFailureReport(input={},storage={}) {
|
|
76
|
+
try {
|
|
77
|
+
const code=typeof input.code==='string' && Object.hasOwn(EXIT_CODES,input.code) && input.code!=='SUCCESS' ? input.code : 'INTERNAL_ERROR';
|
|
78
|
+
const command=COMMANDS.has(input.command)?input.command:'unknown';
|
|
79
|
+
const report={schemaVersion:1,format:'rivet-failure-report',createdAt:new Date().toISOString(),
|
|
80
|
+
versions:{rivet:version(metadata.version),node:version(process.version),platform:PLATFORMS.has(process.platform)?process.platform:'unknown',architecture:ARCHITECTURES.has(process.arch)?process.arch:'unknown'},
|
|
81
|
+
runtimeFingerprint:await runtimeFingerprint(),command:{name:command,subcommand:command!=='unknown'&&SUBCOMMANDS.has(input.subcommand)?input.subcommand:null},
|
|
82
|
+
failure:{code,exitCode:EXIT_CODES[code],causeCode:CAUSE_CODES.has(input.causeCode)?input.causeCode:null},
|
|
83
|
+
sharing:{issueUrl:ISSUE_URL,instructions:'Review this file before attaching it to an issue. Add reproduction steps and expected behavior. No upload is performed.'}};
|
|
84
|
+
return await savePrivate(report,storage);
|
|
85
|
+
} catch { return null; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Persist only the existing allowlisted collector's output, never a raw caller bundle. */
|
|
89
|
+
export async function saveSupportReport(projectRoot,collectorOptions={},storage={}) {
|
|
90
|
+
try {
|
|
91
|
+
const collected=await collectSupportBundle(projectRoot,collectorOptions);
|
|
92
|
+
const report={...collected,format:'rivet-support-report',createdAt:new Date().toISOString(),runtimeFingerprint:await runtimeFingerprint(),
|
|
93
|
+
versions:{...collected.versions,rivet:collected.versions.rivet===null?null:version(metadata.version)}};
|
|
94
|
+
return await savePrivate(report,storage);
|
|
95
|
+
} catch { throw new Error('Support report could not be saved.'); }
|
|
96
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
|
|
4
|
+
import { containsSecretMaterial, immutableJson } from '../clients/contract.js';
|
|
5
|
+
|
|
6
|
+
const INPUT_KEYS = new Set([
|
|
7
|
+
'source', 'title', 'description', 'acceptanceCriteria', 'contextRefs', 'capturedAt',
|
|
8
|
+
]);
|
|
9
|
+
const OPTIONAL_KEYS = ['context', 'criteriaProvenance'];
|
|
10
|
+
const REQUEST_KEYS = new Set(['schemaVersion', ...INPUT_KEYS, 'digest']);
|
|
11
|
+
const SOURCE_KEYS = new Set(['kind', 'ref', 'revision', 'url']);
|
|
12
|
+
const SOURCE_KINDS = new Set(['inline', 'markdown', 'jira', 'linear', 'host-observation']);
|
|
13
|
+
const TICKET = /^[A-Z][A-Z0-9]{0,31}-[1-9][0-9]{0,15}$/;
|
|
14
|
+
const SHA256_REVISION = /^sha256:[a-f0-9]{64}$/;
|
|
15
|
+
const PRIVATE_IPV4 = /^(?:0|10|127|169\.254|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168|224|240)(?:\.|$)/;
|
|
16
|
+
|
|
17
|
+
export class WorkRequestError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super('Work request is invalid.');
|
|
20
|
+
this.name = 'WorkRequestError';
|
|
21
|
+
this.code = 'ERR_INVALID_WORK_REQUEST';
|
|
22
|
+
this.safeMessage = this.message;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fail() { throw new WorkRequestError(); }
|
|
27
|
+
|
|
28
|
+
function captureRecord(input, allowed, required = allowed) {
|
|
29
|
+
let array;
|
|
30
|
+
let prototype;
|
|
31
|
+
let keys;
|
|
32
|
+
try {
|
|
33
|
+
array = Array.isArray(input);
|
|
34
|
+
prototype = Object.getPrototypeOf(input);
|
|
35
|
+
keys = Reflect.ownKeys(input);
|
|
36
|
+
} catch { fail(); }
|
|
37
|
+
if (!input || typeof input !== 'object' || array
|
|
38
|
+
|| (prototype !== Object.prototype && prototype !== null)
|
|
39
|
+
|| keys.length > allowed.size || keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail();
|
|
40
|
+
const output = Object.create(null);
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
let descriptor;
|
|
43
|
+
let value;
|
|
44
|
+
try {
|
|
45
|
+
descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
46
|
+
value = input[key];
|
|
47
|
+
} catch { fail(); }
|
|
48
|
+
if (!descriptor?.enumerable) fail();
|
|
49
|
+
output[key] = value;
|
|
50
|
+
}
|
|
51
|
+
if ([...required].some(key => !Object.hasOwn(output, key))) fail();
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function string(value, maximum) {
|
|
56
|
+
if (typeof value !== 'string' || value.length < 1 || value.length > maximum
|
|
57
|
+
|| value.normalize('NFKC') !== value || /[\u0000\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)
|
|
58
|
+
|| containsSecretMaterial(value)) fail();
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function list(value, { maximum, itemMaximum }) {
|
|
63
|
+
let array;
|
|
64
|
+
let prototype;
|
|
65
|
+
let length;
|
|
66
|
+
let keys;
|
|
67
|
+
try {
|
|
68
|
+
array = Array.isArray(value);
|
|
69
|
+
prototype = Object.getPrototypeOf(value);
|
|
70
|
+
length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
|
|
71
|
+
keys = Reflect.ownKeys(value);
|
|
72
|
+
} catch { fail(); }
|
|
73
|
+
if (!array || prototype !== Array.prototype || !Number.isSafeInteger(length)
|
|
74
|
+
|| length < 0 || length > maximum || keys.length !== length + 1) fail();
|
|
75
|
+
const output = [];
|
|
76
|
+
for (let index = 0; index < length; index += 1) {
|
|
77
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
78
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail();
|
|
79
|
+
output.push(string(descriptor.value, itemMaximum));
|
|
80
|
+
}
|
|
81
|
+
if (new Set(output).size !== output.length) fail();
|
|
82
|
+
return Object.freeze(output);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// User additions remain explicit, bounded data, never inferred tracker content.
|
|
86
|
+
export function userAcceptanceCriteria(input) {
|
|
87
|
+
const values = list(input, { maximum: 256, itemMaximum: 4096 });
|
|
88
|
+
if (!values.length || Buffer.byteLength(JSON.stringify(values), 'utf8') > 64 * 1024
|
|
89
|
+
|| values.some(value => !value.trim() || /[\u0000-\u001f\u007f]/.test(value))) fail();
|
|
90
|
+
return values;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseAcceptanceCriteriaText(input) {
|
|
94
|
+
if (typeof input !== 'string' || Buffer.byteLength(input, 'utf8') > 64 * 1024
|
|
95
|
+
|| /[\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f]/.test(input)) fail();
|
|
96
|
+
const normalized = input.replaceAll('\r\n', '\n');
|
|
97
|
+
if (normalized.includes('\r')) fail();
|
|
98
|
+
return userAcceptanceCriteria(normalized.split('\n').map(value => value.trim()));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function criteriaProvenance(input, output) {
|
|
102
|
+
const value = captureRecord(input, new Set(['sourceAcceptanceCriteria', 'userAcceptanceCriteria']));
|
|
103
|
+
if (!['jira', 'linear'].includes(output.source.kind)) fail();
|
|
104
|
+
const sourceAcceptanceCriteria = list(value.sourceAcceptanceCriteria, { maximum: 256, itemMaximum: 4096 });
|
|
105
|
+
const additions = userAcceptanceCriteria(value.userAcceptanceCriteria);
|
|
106
|
+
if (JSON.stringify([...new Set([...sourceAcceptanceCriteria, ...additions])]) !== JSON.stringify(output.acceptanceCriteria)) fail();
|
|
107
|
+
return Object.freeze({ sourceAcceptanceCriteria, userAcceptanceCriteria: additions });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeRelativePath(value) {
|
|
111
|
+
const normalized = string(value, 512).replaceAll('\\', '/');
|
|
112
|
+
if (normalized === '.' || normalized.startsWith('/') || normalized.endsWith('/')
|
|
113
|
+
|| normalized.includes('//') || /^[A-Za-z]:/.test(normalized)
|
|
114
|
+
|| normalized.split('/').some(part => !part || part === '.' || part === '..')) fail();
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function safeUrl(value, host = false) {
|
|
119
|
+
string(value, 2048);
|
|
120
|
+
let parsed;
|
|
121
|
+
try { parsed = new URL(value); } catch { fail(); }
|
|
122
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
123
|
+
if (parsed.protocol !== 'https:' || parsed.username || parsed.password || (!host && parsed.search) || parsed.hash
|
|
124
|
+
|| hostname === 'localhost' || hostname.endsWith('.localhost')
|
|
125
|
+
|| (isIP(hostname) === 4 && PRIVATE_IPV4.test(hostname))
|
|
126
|
+
|| (isIP(hostname) === 6 && (hostname === '::1' || hostname.startsWith('fc') || hostname.startsWith('fd') || hostname.startsWith('fe80:')))) fail();
|
|
127
|
+
if (host && [...parsed.searchParams].some(([key, item]) => !['node-id', 'pageId'].includes(key) || !/^[A-Za-z0-9:_-]{1,128}$/.test(item))) fail();
|
|
128
|
+
return parsed.toString();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function timestamp(value) {
|
|
132
|
+
string(value, 64);
|
|
133
|
+
const date = new Date(value);
|
|
134
|
+
if (Number.isNaN(date.getTime()) || date.toISOString() !== value) fail();
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function source(input) {
|
|
139
|
+
const value = captureRecord(input, SOURCE_KEYS, new Set(['kind', 'ref']));
|
|
140
|
+
const kind = string(value.kind, 16);
|
|
141
|
+
if (!SOURCE_KINDS.has(kind)) fail();
|
|
142
|
+
let ref = string(value.ref, 512);
|
|
143
|
+
if (kind === 'inline' && ref !== 'inline') fail();
|
|
144
|
+
if (kind === 'markdown') ref = safeRelativePath(ref);
|
|
145
|
+
if ((kind === 'jira' || kind === 'linear') && !TICKET.test(ref)) fail();
|
|
146
|
+
let revision;
|
|
147
|
+
if (value.revision !== undefined) {
|
|
148
|
+
revision = string(value.revision, 256);
|
|
149
|
+
if (kind === 'markdown' && !SHA256_REVISION.test(revision)) fail();
|
|
150
|
+
}
|
|
151
|
+
const result = {
|
|
152
|
+
kind,
|
|
153
|
+
ref,
|
|
154
|
+
...(revision === undefined ? {} : { revision }),
|
|
155
|
+
...(value.url === undefined ? {} : { url: safeUrl(value.url, kind === 'host-observation') }),
|
|
156
|
+
};
|
|
157
|
+
if ((kind === 'inline' || kind === 'markdown') && result.url !== undefined) fail();
|
|
158
|
+
return Object.freeze(result);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function canonical(value) {
|
|
162
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
163
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
164
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function context(input) {
|
|
168
|
+
let value;
|
|
169
|
+
try { value = immutableJson(input); } catch { fail(); }
|
|
170
|
+
if (Buffer.byteLength(JSON.stringify(value), 'utf8') > 64 * 1024) fail();
|
|
171
|
+
captureRecord(value, new Set(['sources', 'userAcceptanceCriteria']));
|
|
172
|
+
if (!Array.isArray(value.sources) || value.sources.length < 1 || value.sources.length > 16) fail();
|
|
173
|
+
const identities = new Set();
|
|
174
|
+
for (const item of value.sources) {
|
|
175
|
+
captureRecord(item, new Set(['provider', 'providerId', 'projectId', 'resourceId', 'url', 'revision',
|
|
176
|
+
'capturedAt', 'transport', 'assurance', 'tool', 'content', 'contentDigest']));
|
|
177
|
+
if (!['jira', 'linear', 'figma', 'confluence', 'generic'].includes(item.provider)
|
|
178
|
+
|| item.transport !== 'harness-mcp' || item.assurance !== 'harness-observed') fail();
|
|
179
|
+
for (const key of ['providerId', 'projectId', 'resourceId', 'revision', 'tool']) string(item[key], 200);
|
|
180
|
+
safeUrl(item.url, true);
|
|
181
|
+
timestamp(item.capturedAt);
|
|
182
|
+
if (!item.content || typeof item.content !== 'object' || Array.isArray(item.content)
|
|
183
|
+
|| containsSecretMaterial(JSON.stringify(item.content))) fail();
|
|
184
|
+
const digest = createHash('sha256').update(canonical(item.content)).digest('hex');
|
|
185
|
+
if (item.contentDigest !== digest) fail();
|
|
186
|
+
const identity = `${item.providerId}:${item.resourceId}`;
|
|
187
|
+
if (identities.has(identity)) fail();
|
|
188
|
+
identities.add(identity);
|
|
189
|
+
}
|
|
190
|
+
list(value.userAcceptanceCriteria, { maximum: 256, itemMaximum: 4096 });
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function capturedInput(input, request = false) {
|
|
195
|
+
const required = request ? REQUEST_KEYS : INPUT_KEYS;
|
|
196
|
+
const value = captureRecord(input, new Set([...required, ...OPTIONAL_KEYS]), required);
|
|
197
|
+
if (request && value.schemaVersion !== 1) fail();
|
|
198
|
+
const acceptanceCriteria = list(value.acceptanceCriteria, { maximum: 256, itemMaximum: 4096 });
|
|
199
|
+
if (acceptanceCriteria.length === 0) fail();
|
|
200
|
+
const contextRefs = list(value.contextRefs, { maximum: 256, itemMaximum: 2048 });
|
|
201
|
+
const output = {
|
|
202
|
+
schemaVersion: 1,
|
|
203
|
+
source: source(value.source),
|
|
204
|
+
title: string(value.title, 500),
|
|
205
|
+
description: string(value.description, 131072),
|
|
206
|
+
acceptanceCriteria,
|
|
207
|
+
contextRefs,
|
|
208
|
+
capturedAt: timestamp(value.capturedAt),
|
|
209
|
+
};
|
|
210
|
+
if (value.context !== undefined) output.context = context(value.context);
|
|
211
|
+
if (value.criteriaProvenance !== undefined) output.criteriaProvenance = criteriaProvenance(value.criteriaProvenance, output);
|
|
212
|
+
if (output.source.kind === 'host-observation') {
|
|
213
|
+
const primary = output.context?.sources[0];
|
|
214
|
+
if (!primary || output.source.ref !== `${primary.providerId}:${primary.resourceId}`
|
|
215
|
+
|| output.source.revision !== `sha256:${primary.contentDigest}` || output.source.url !== primary.url) fail();
|
|
216
|
+
}
|
|
217
|
+
if (request) output.digest = string(value.digest, 64);
|
|
218
|
+
return output;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function digestPayload(value) {
|
|
222
|
+
return {
|
|
223
|
+
schemaVersion: value.schemaVersion,
|
|
224
|
+
source: value.source,
|
|
225
|
+
title: value.title,
|
|
226
|
+
description: value.description,
|
|
227
|
+
acceptanceCriteria: value.acceptanceCriteria,
|
|
228
|
+
contextRefs: value.contextRefs,
|
|
229
|
+
capturedAt: value.capturedAt,
|
|
230
|
+
...(value.context === undefined ? {} : { context: value.context }),
|
|
231
|
+
...(value.criteriaProvenance === undefined ? {} : { criteriaProvenance: value.criteriaProvenance }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function workRequestDigest(input) {
|
|
236
|
+
const value = capturedInput(input, Object.hasOwn(input ?? {}, 'digest'));
|
|
237
|
+
return createHash('sha256').update(canonical(digestPayload(value))).digest('hex');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function createWorkRequest(input) {
|
|
241
|
+
const value = capturedInput(input);
|
|
242
|
+
return Object.freeze({ ...value, digest: createHash('sha256').update(canonical(value)).digest('hex') });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function validateWorkRequest(input) {
|
|
246
|
+
const value = capturedInput(input, true);
|
|
247
|
+
if (!/^[a-f0-9]{64}$/.test(value.digest)
|
|
248
|
+
|| createHash('sha256').update(canonical(digestPayload(value))).digest('hex') !== value.digest) fail();
|
|
249
|
+
return input;
|
|
250
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createIntegrationRegistry } from '../integrations/registry.js';
|
|
2
|
+
import { normalizeHostObservation } from '../integrations/host-observation.js';
|
|
3
|
+
import { fields, snapshot } from '../integrations/capabilities.js';
|
|
4
|
+
import { createWorkRequest, WorkRequestError } from './contract.js';
|
|
5
|
+
|
|
6
|
+
const CAPABILITIES = { jira: 'issues-read', linear: 'issues-read', figma: 'files-read', confluence: 'pages-read', generic: 'context-read' };
|
|
7
|
+
const TICKET = /^[A-Z][A-Z0-9]{0,31}-[1-9][0-9]{0,15}$/;
|
|
8
|
+
function fail() { throw new WorkRequestError(); }
|
|
9
|
+
|
|
10
|
+
function requestId(value, provider) {
|
|
11
|
+
if (typeof value !== 'string') fail();
|
|
12
|
+
if (!value.startsWith('https://')) return value;
|
|
13
|
+
let url;
|
|
14
|
+
try { url = new URL(value); } catch { fail(); }
|
|
15
|
+
if (!provider.endpoint || url.origin !== new URL(provider.endpoint).origin || url.username || url.password || url.search || url.hash) fail();
|
|
16
|
+
const expression = provider.kind === 'jira' ? /^\/browse\/([A-Z][A-Z0-9]*-[1-9][0-9]*)\/?$/
|
|
17
|
+
: provider.kind === 'linear' ? /^\/[^/]+\/issue\/([A-Z][A-Z0-9]*-[1-9][0-9]*)(?:\/[^/]+)?\/?$/ : null;
|
|
18
|
+
const match = expression?.exec(url.pathname);
|
|
19
|
+
if (!match) fail();
|
|
20
|
+
return match[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveHostWorkRequest({ config, bundle: raw, capturedAt }) {
|
|
24
|
+
const bundle = snapshot(raw);
|
|
25
|
+
fields(bundle, ['schemaVersion', 'projectId', 'host', 'request', 'observations', 'userAcceptanceCriteria'],
|
|
26
|
+
['schemaVersion', 'projectId', 'host', 'request', 'observations']);
|
|
27
|
+
if (bundle.schemaVersion !== 1 || bundle.projectId !== config.project.id
|
|
28
|
+
|| !Array.isArray(bundle.observations) || bundle.observations.length < 1 || bundle.observations.length > 16) fail();
|
|
29
|
+
fields(bundle.request, ['providerId', 'resourceId']);
|
|
30
|
+
const registry = createIntegrationRegistry({ config, projectId: config.project.id, host: bundle.host });
|
|
31
|
+
const primaryProvider = registry.list().find(p => p.id === bundle.request.providerId);
|
|
32
|
+
if (!primaryProvider || !['jira', 'linear'].includes(primaryProvider.kind)) fail();
|
|
33
|
+
const primaryId = requestId(bundle.request.resourceId, primaryProvider);
|
|
34
|
+
if (!TICKET.test(primaryId)) fail();
|
|
35
|
+
const sources = bundle.observations.map(observation => {
|
|
36
|
+
const descriptor = registry.list().find(p => p.id === observation.providerId);
|
|
37
|
+
if (!descriptor || !CAPABILITIES[descriptor.kind]) fail();
|
|
38
|
+
const provider = registry.resolve({ providerId: descriptor.id, capability: CAPABILITIES[descriptor.kind], tool: observation.tool });
|
|
39
|
+
const normalized = normalizeHostObservation(observation, { provider, projectId: config.project.id });
|
|
40
|
+
const { schemaVersion, sourceUrl, ...source } = normalized;
|
|
41
|
+
if (!source.content || typeof source.content !== 'object' || Array.isArray(source.content)) fail();
|
|
42
|
+
const url = new URL(sourceUrl);
|
|
43
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
44
|
+
if (source.provider === 'jira' && (segments.length !== 2 || segments[0] !== 'browse' || segments[1] !== source.resourceId)) fail();
|
|
45
|
+
if (source.provider === 'linear' && (segments[1] !== 'issue' || segments[2] !== source.resourceId)) fail();
|
|
46
|
+
if (source.provider === 'figma' && (!['file', 'design', 'board', 'proto'].includes(segments[0]) || segments[1] !== source.resourceId)) fail();
|
|
47
|
+
if (source.provider === 'confluence' && !(url.searchParams.get('pageId') === source.resourceId
|
|
48
|
+
|| segments.some((part, index) => part === 'pages' && segments[index + 1] === source.resourceId))) fail();
|
|
49
|
+
if (['jira', 'linear'].includes(source.provider)) {
|
|
50
|
+
fields(source.content, ['title', 'description', 'acceptanceCriteria']);
|
|
51
|
+
if (!Array.isArray(source.content.acceptanceCriteria) || !TICKET.test(source.resourceId)) fail();
|
|
52
|
+
} else {
|
|
53
|
+
fields(source.content, ['title', 'text']);
|
|
54
|
+
if (typeof source.content.title !== 'string' || !source.content.title || typeof source.content.text !== 'string' || !source.content.text) fail();
|
|
55
|
+
}
|
|
56
|
+
return { ...source, url: sourceUrl };
|
|
57
|
+
});
|
|
58
|
+
const selected = sources.filter(s => s.providerId === bundle.request.providerId && s.resourceId === primaryId);
|
|
59
|
+
if (selected.length !== 1) fail();
|
|
60
|
+
const primary = selected[0];
|
|
61
|
+
const additions = bundle.userAcceptanceCriteria ?? [];
|
|
62
|
+
if (!Array.isArray(additions)) fail();
|
|
63
|
+
const acceptanceCriteria = [...new Set([...primary.content.acceptanceCriteria, ...additions])];
|
|
64
|
+
if (!acceptanceCriteria.length) {
|
|
65
|
+
const error = new WorkRequestError();
|
|
66
|
+
error.safeMessage = 'The source has no acceptance criteria. Ask the user and supply explicit userAcceptanceCriteria before proposing work.';
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return createWorkRequest({
|
|
70
|
+
source: { kind: 'host-observation', ref: `${primary.providerId}:${primary.resourceId}`, revision: `sha256:${primary.contentDigest}`, url: primary.url },
|
|
71
|
+
title: primary.content.title,
|
|
72
|
+
description: primary.content.description,
|
|
73
|
+
acceptanceCriteria,
|
|
74
|
+
contextRefs: sources.map(s => `${s.providerId}:${s.resourceId}:sha256:${s.contentDigest}`),
|
|
75
|
+
capturedAt,
|
|
76
|
+
context: { sources: [primary, ...sources.filter(s => s !== primary)], userAcceptanceCriteria: additions },
|
|
77
|
+
});
|
|
78
|
+
}
|