@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.
Files changed (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,142 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path';
4
+ import { CliError } from '../cli/output.js';
5
+ import { createMutationGuard, ensureContainedDirectory, findProjectRoot, withPinnedTargetDirectory } from '../commands/install.js';
6
+ import { inspectManagedInstall, managedInstall } from './managed.js';
7
+ import { acquireLock, recoverAbandonedLock, StaleLockError } from '../state/lock.js';
8
+ import integrity from './runtime-integrity.cjs';
9
+ import { launcher, reference, runtimeSkill } from './project-reference.js';
10
+ const { runtimeIntegrity } = integrity;
11
+ const NAME = '.rivet.cjs', HASH = /^[a-f0-9]{64}$/;
12
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
13
+ const fail = message => { throw new CliError(message, 'REPOSITORY_CONFLICT'); };
14
+ function regular(file, fs, maximum = 16 * 1024 * 1024) {
15
+ const before = fs.lstatSync(file);
16
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maximum) fail('Project runtime contains an unsafe file.');
17
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
18
+ try {
19
+ const opened = fs.fstatSync(fd), bytes = Buffer.alloc(before.size + 1), length = fs.readSync(fd, bytes, 0, bytes.length, 0), after = fs.fstatSync(fd);
20
+ if (opened.dev !== before.dev || opened.ino !== before.ino || length !== before.size || after.size !== before.size
21
+ || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) fail('Project runtime file changed during validation.');
22
+ return bytes.subarray(0, length);
23
+ } finally { fs.closeSync(fd); }
24
+ }
25
+ function sourceSnapshot(packageRoot, fs) {
26
+ const metadataBytes = regular(join(packageRoot, 'package.json'), fs, 65536), pkg = JSON.parse(metadataBytes);
27
+ if (pkg.name !== '@agilno-tech/rivet' || typeof pkg.version !== 'string' || !Array.isArray(pkg.files)
28
+ || !['bin/cli.js', './bin/cli.js'].includes(pkg.bin?.rivet)
29
+ || ['prepare', 'prepack', 'postpack'].some(key => Object.hasOwn(pkg.scripts ?? {}, key))) fail('The running Rivet package cannot be safely pinned.');
30
+ const files = new Map([['package.json', { bytes: metadataBytes, mode: 0o644 }]]); let total = metadataBytes.length;
31
+ const visit = ref => {
32
+ if (typeof ref !== 'string' || !ref || isAbsolute(ref) || /[\u0000-\u001f\u007f\\*?]/.test(ref)
33
+ || ref.split('/').some(part => !part || part === '.' || part === '..' || part === 'node_modules' || part === '.git')) fail('Unsupported runtime package file selection.');
34
+ const file = join(packageRoot, ref), stat = fs.lstatSync(file);
35
+ if (stat.isSymbolicLink()) fail('The running Rivet package contains unsupported links.');
36
+ if (stat.isDirectory()) { for (const child of fs.readdirSync(file).sort()) visit(ref + '/' + child); return; }
37
+ const bytes = regular(file, fs);total += bytes.length;
38
+ if (files.size >= 20000 || total > 64 * 1024 * 1024) fail('The running Rivet package exceeds snapshot limits.');
39
+ files.set(ref, { bytes, mode: stat.mode & 0o111 ? 0o755 : 0o644 });
40
+ };
41
+ for (const ref of pkg.files) visit(ref.replace(/\/$/, ''));
42
+ const entries = [...files].sort(([a],[b]) => Buffer.compare(Buffer.from(a),Buffer.from(b)));
43
+ return { pkg, files: entries, id: hash(JSON.stringify(entries.map(([path, file]) => ({ path, mode: file.mode, sha256: hash(file.bytes) })))) };
44
+ }
45
+ async function command(executable, args, options) {
46
+ return new Promise((resolveCommand, reject) => {
47
+ let output = '', errors = '';const child = spawn(executable, args, { cwd: options.cwd, env: options.env, shell: false, detached: true, stdio: ['ignore','pipe','pipe'] });
48
+ const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} };
49
+ const timer = setTimeout(kill, 180000);const abort = () => kill();options.signal?.addEventListener('abort', abort, { once: true });
50
+ if (options.signal?.aborted) kill();
51
+ child.stdout.on('data', bytes => { output += bytes; if (output.length > 2 * 1024 * 1024) kill(); });
52
+ child.stderr.on('data', bytes => { errors += bytes; if (errors.length > 2 * 1024 * 1024) kill(); });
53
+ child.on('error', reject);child.on('close', code => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); if (code !== 0 || options.signal?.aborted) reject(new Error('Private runtime package preparation failed or was interrupted.')); else resolveCommand(output); });
54
+ });
55
+ }
56
+ async function lock(path) {
57
+ try { return await acquireLock(path); }
58
+ catch (error) { if (!(error instanceof StaleLockError)) throw error; await recoverAbandonedLock(path); return acquireLock(path); }
59
+ }
60
+ function verifyReference(root, before, fs) {
61
+ const after = reference(root, fs);
62
+ if (Boolean(before) !== Boolean(after) || (before && (before.id !== after.id || before.dev !== after.dev || before.ino !== after.ino))) fail('Project runtime reference changed during installation.');
63
+ }
64
+ export async function projectRuntimeInstall(parsed, dependencies) {
65
+ if (Number.parseInt(process.versions.node.split('.')[0], 10) < 22) throw new CliError('Node.js 22 or newer is required for a project runtime. Select a supported Node runtime and retry installation.', 'INVALID_INPUT');
66
+ const { fs } = dependencies;
67
+ if (parsed.operands?.length || parsed.flags.global || parsed.flags.all || Object.keys(parsed.flags).some(key => !['project-runtime','minimal','project','target','claude','codex','json'].includes(key))) throw new CliError('Use install/uninstall --project-runtime [--project=<path>] [--target=both|claude|codex].', 'INVALID_INPUT');
68
+ const selected = parsed.flags.project === undefined ? findProjectRoot(dependencies.cwd(), fs) : resolve(dependencies.cwd(), parsed.flags.project);
69
+ if (!selected || fs.lstatSync(selected).isSymbolicLink() || !fs.lstatSync(selected).isDirectory()) fail('Select an existing project directory.');
70
+ const root = fs.realpathSync(selected), home = fs.realpathSync(dependencies.home());
71
+ const projectGuard = createMutationGuard(root, root, fs), rootIdentity = fs.lstatSync(root);
72
+ const cache = join(home,'.cache','rivet','project-runtimes'), cacheGuard = createMutationGuard(home,cache,fs);
73
+ const before = reference(root, fs), source = sourceSnapshot(dependencies.packageRoot,fs);
74
+ const skillBytes = regular(join(dependencies.packageRoot,'templates','harness','SKILL.md'),fs,512*1024);
75
+ const managedParsed = { ...parsed, flags: { ...parsed.flags, minimal: true, project: root } };
76
+ const quiet = {log(){},error(){},json(){}};
77
+ // Validate every selected owned target before any package installation.
78
+ inspectManagedInstall(managedParsed,{...dependencies,managedSkillBytes:runtimeSkill(skillBytes,before?.id ?? source.id)});
79
+ const controller = new AbortController(), abort=()=>controller.abort();process.on('SIGINT',abort);process.on('SIGTERM',abort);
80
+ let projectLock, cacheLock, stage;
81
+ const completed = await (async () => {
82
+ try {
83
+ projectGuard.assertPath(root);projectLock = await lock(join(root,'.rivet-project-runtime.lock'));
84
+ verifyReference(root,before,fs);
85
+ if (parsed.command === 'uninstall') {
86
+ await managedInstall(managedParsed,{...dependencies,output:quiet});
87
+ // A remaining managed harness still needs the reference.
88
+ const keep = ['.claude','.agents'].some(target=>fs.existsSync(join(root,target,'skills','rivet','.rivet-install.json')));
89
+ if (!keep && before) withPinnedTargetDirectory(root,rootIdentity,fs,()=>{verifyReference(root,before,fs);fs.unlinkSync(NAME);});
90
+ const result={scope:'project',runtimeReference:keep?'retained':'removed',cache:'retained'};
91
+ return result;
92
+ }
93
+ const cacheIdentity=ensureContainedDirectory(cacheGuard,fs);fs.chmodSync(cache,0o700);cacheLock=await lock(join(cache,'.install.lock'));
94
+ cacheGuard.assertPath(cache);
95
+ const indexFile=join(cache,`source-${source.id}-${process.platform}-${process.arch}.json`);
96
+ let runtime;
97
+ if(fs.existsSync(indexFile)){
98
+ const id=JSON.parse(regular(indexFile,fs,1024)).runtimeId;if(!HASH.test(id))fail('Private runtime cache index is invalid.');
99
+ runtime=runtimeIntegrity(join(cache,id),id,source.id);if(runtime.metadata.sourceDigest!==source.id)fail('Private runtime source identity changed.');
100
+ }else{
101
+ stage=join(cache,`.stage-${randomUUID()}`);fs.mkdirSync(stage,{mode:0o700});const snapshot=join(stage,'source'),destination=join(stage,'runtime');fs.mkdirSync(snapshot);fs.mkdirSync(destination);
102
+ for(const [ref,file] of source.files){const target=join(snapshot,ref);fs.mkdirSync(dirname(target),{recursive:true});fs.writeFileSync(target,file.bytes,{flag:'wx',mode:file.mode});}
103
+ const env={...dependencies.env,PATH:dependencies.env?.PATH??process.env.PATH,npm_config_ignore_scripts:'true',npm_config_update_notifier:'false'};
104
+ const run=dependencies.runtimeCommand??command, options={cwd:'.',env,signal:controller.signal};
105
+ const packed=JSON.parse(await withPinnedTargetDirectory(snapshot,fs.lstatSync(snapshot),fs,()=>run('npm',['pack','--ignore-scripts','--json','--pack-destination','..'],options)));
106
+ if(!Array.isArray(packed)||packed.length!==1||typeof packed[0].filename!=='string'||!/^[A-Za-z0-9._-]+\.tgz$/.test(packed[0].filename))fail('Private package preparation returned an invalid artifact.');
107
+ const artifact=join(stage,packed[0].filename),artifactDigest=hash(regular(artifact,fs,64*1024*1024));
108
+ await withPinnedTargetDirectory(stage,fs.lstatSync(stage),fs,()=>run('npm',['install','--prefix','runtime','--omit=dev','--ignore-scripts','--install-links','--no-audit','--no-fund','./'+packed[0].filename],options));
109
+ if(controller.signal.aborted)fail('Project runtime installation was interrupted; existing reference preserved.');
110
+ const metadata={schemaVersion:1,sourceDigest:source.id,artifactDigest,package:{name:source.pkg.name,version:source.pkg.version},platform:process.platform,architecture:process.arch};
111
+ fs.writeFileSync(join(destination,'.rivet-runtime.json'),JSON.stringify(metadata)+'\n',{flag:'wx',mode:0o600});runtime=runtimeIntegrity(destination);
112
+ withPinnedTargetDirectory(cache,cacheIdentity,fs,()=>{
113
+ cacheGuard.assertPath(cache);if(fs.existsSync(runtime.id))runtimeIntegrity(join(cache,runtime.id),runtime.id);else fs.renameSync(join(relative(cache,stage),'runtime'),runtime.id);
114
+ if(fs.existsSync(basename(indexFile)))fail('Private runtime cache index changed during preparation.');
115
+ const indexStage=join(relative(cache,stage),'index.json');
116
+ fs.writeFileSync(indexStage,JSON.stringify({runtimeId:runtime.id})+'\n',{flag:'wx',mode:0o600});fs.renameSync(indexStage,basename(indexFile));
117
+ });
118
+ }
119
+ const managedDependencies={...dependencies,managedSkillBytes:runtimeSkill(skillBytes,source.id),output:quiet};
120
+ verifyReference(root,before,fs);inspectManagedInstall(managedParsed,managedDependencies);
121
+ // Reference first: any partial skill update still has a working pinned command.
122
+ withPinnedTargetDirectory(root,rootIdentity,fs,()=>{
123
+ verifyReference(root,before,fs);const name=`.rivet-reference-${randomUUID()}.tmp`;
124
+ try{fs.writeFileSync(name,launcher(source.id),{flag:'wx',mode:0o644});fs.renameSync(name,NAME);}finally{if(fs.existsSync(name))fs.unlinkSync(name);}
125
+ });
126
+ try{await managedInstall(managedParsed,managedDependencies);}catch(error){throw new CliError('Pinned runtime reference is installed, but harness instruction installation is incomplete. Existing user edits were preserved; retry after resolving the reported conflict.','REPOSITORY_CONFLICT',{cause:error});}
127
+ const result={scope:'project',runtimeId:runtime.id,sourceDigest:source.id,reference:join(root,NAME),cache:join(cache,runtime.id),invocation:'node .rivet.cjs'};
128
+ return result;
129
+ }catch(error){if(error instanceof CliError)throw error;throw new CliError('Project runtime installation stopped. Existing application files are preserved. Inspect the private cache or installation lock before retrying.','REPOSITORY_CONFLICT',{cause:error});}
130
+ finally{
131
+ let cleanupError;
132
+ if(stage){try{cacheGuard.assertPath(stage);fs.rmSync(stage,{recursive:true,force:true});}catch(error){cleanupError=error;}}
133
+ try{await cacheLock?.release();}catch(error){cleanupError??=error;}
134
+ try{await projectLock?.release();}catch(error){cleanupError??=error;}
135
+ process.off('SIGINT',abort);process.off('SIGTERM',abort);
136
+ if(cleanupError)throw new CliError('Project runtime operation may have completed, but private staging or lock cleanup is incomplete. Inspect the project reference and private cache before retrying.','REPOSITORY_CONFLICT',{cause:cleanupError});
137
+ }
138
+ })();
139
+ if(parsed.flags.json)dependencies.output.json({ok:true,command:parsed.command,result:completed});
140
+ else dependencies.output.log(parsed.command==='uninstall'?'Removed selected owned project runtime instructions. Shared runtime cache retained.':'Pinned project runtime installed. From the project root: node .rivet.cjs setup. No global Rivet installation is required.');
141
+ return 0;
142
+ }
@@ -0,0 +1,141 @@
1
+ // Self-contained so the generated project reference can verify a cache before
2
+ // importing any cached JavaScript. Keep all dependencies inside this function.
3
+ function runtimeIntegrity(root, expectedId, expectedSource) {
4
+ const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'), os = require('node:os');
5
+ const fail = () => { throw new Error('Pinned Rivet runtime is missing or changed. Reinstall the approved project runtime.'); };
6
+ const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
7
+ const inside = value => value === root || (value.startsWith(root + path.sep));
8
+ const rootStat = fs.lstatSync(root);
9
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || fs.realpathSync(root) !== root) fail();
10
+ const manifestPath = path.join(root, '.rivet-runtime.json');
11
+ const regular = (file, maximum) => {
12
+ const before = fs.lstatSync(file);
13
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maximum) fail();
14
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
15
+ try {
16
+ const opened = fs.fstatSync(fd);
17
+ if (opened.dev !== before.dev || opened.ino !== before.ino) fail();
18
+ const bytes = Buffer.alloc(before.size + 1), length = fs.readSync(fd, bytes, 0, bytes.length, 0);
19
+ const after = fs.fstatSync(fd);
20
+ if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) fail();
21
+ return bytes.subarray(0, length);
22
+ } finally { fs.closeSync(fd); }
23
+ };
24
+ const metadata = JSON.parse(regular(manifestPath, 16384).toString('utf8'));
25
+ if (metadata.schemaVersion !== 1 || metadata.platform !== process.platform || metadata.architecture !== process.arch
26
+ || !/^[a-f0-9]{64}$/.test(metadata.sourceDigest) || !/^[a-f0-9]{64}$/.test(metadata.artifactDigest)
27
+ || !['@agilno-tech/rivet', '@agilno/rivet'].includes(metadata.package?.name)) fail();
28
+ const entries = []; let total = 0;
29
+ const visit = (directory, relative = '') => {
30
+ for (const name of fs.readdirSync(directory).sort()) {
31
+ if (name === '.rivet-runtime.json' && !relative) continue;
32
+ if (!name || /[\u0000-\u001f\u007f\\]/.test(name)) fail();
33
+ const ref = relative ? relative + '/' + name : name, file = path.join(directory, name), stat = fs.lstatSync(file);
34
+ if (entries.length >= 20000) fail();
35
+ if (stat.isSymbolicLink()) {
36
+ if (!ref.startsWith('node_modules/') || !inside(fs.realpathSync(file))) fail();
37
+ entries.push({ path: ref, kind: 'link', target: fs.readlinkSync(file) });
38
+ } else if (stat.isDirectory()) { entries.push({ path: ref, kind: 'directory' }); visit(file, ref); }
39
+ else {
40
+ const bytes = regular(file, 16 * 1024 * 1024); total += bytes.length;
41
+ if (total > 128 * 1024 * 1024) fail();
42
+ entries.push({ path: ref, kind: 'file', executable: Boolean(stat.mode & 0o111), sha256: digest(bytes) });
43
+ }
44
+ }
45
+ };
46
+ visit(root);
47
+ const id = digest(JSON.stringify({ metadata, entries }));
48
+ if (expectedId !== undefined && id !== expectedId) fail();
49
+ const packageRoot = path.join(root, 'node_modules', ...metadata.package.name.split('/'));
50
+ const entry = path.join(packageRoot, 'bin', 'cli.js');
51
+ if (!inside(fs.realpathSync(entry))) fail();
52
+ const pkg = JSON.parse(regular(path.join(packageRoot, 'package.json'), 65536));
53
+ if (pkg.name !== metadata.package.name || pkg.version !== metadata.package.version || !Array.isArray(pkg.files)) fail();
54
+ // Bind the installed Rivet code to the portable project source pin. Runtime
55
+ // dependencies and platform remain independently pinned in the private cache.
56
+ const sourceFiles = new Map();
57
+ const sourceVisit = ref => {
58
+ if (typeof ref !== 'string' || !ref || path.isAbsolute(ref) || /[\u0000-\u001f\u007f\\*?]/.test(ref)
59
+ || ref.split('/').some(part => !part || part === '.' || part === '..' || part === '.git' || part === 'node_modules')) fail();
60
+ const file = path.join(packageRoot, ref), stat = fs.lstatSync(file);
61
+ if (stat.isSymbolicLink()) fail();
62
+ if (stat.isDirectory()) { for (const name of fs.readdirSync(file).sort()) sourceVisit(ref + '/' + name); return; }
63
+ sourceFiles.set(ref, { path: ref, mode: stat.mode & 0o111 ? 0o755 : 0o644, sha256: digest(regular(file,16*1024*1024)) });
64
+ };
65
+ sourceVisit('package.json');
66
+ for (const ref of pkg.files) sourceVisit(ref.replace(/\/$/, ''));
67
+ const sourceDigest = digest(JSON.stringify([...sourceFiles.values()].sort((a,b)=>Buffer.compare(Buffer.from(a.path),Buffer.from(b.path)))));
68
+ if (sourceDigest !== metadata.sourceDigest || (expectedSource !== undefined && sourceDigest !== expectedSource)) fail();
69
+ return { id, entry, metadata };
70
+ }
71
+ // Frozen ownership renderer for unchanged project references from the former scope.
72
+ // Do not edit this function: its exact source bytes are embedded in those files.
73
+ const legacyRuntimeIntegrity = function runtimeIntegrity(root, expectedId, expectedSource) {
74
+ const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'), os = require('node:os');
75
+ const fail = () => { throw new Error('Pinned Rivet runtime is missing or changed. Reinstall the approved project runtime.'); };
76
+ const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
77
+ const inside = value => value === root || (value.startsWith(root + path.sep));
78
+ const rootStat = fs.lstatSync(root);
79
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || fs.realpathSync(root) !== root) fail();
80
+ const manifestPath = path.join(root, '.rivet-runtime.json');
81
+ const regular = (file, maximum) => {
82
+ const before = fs.lstatSync(file);
83
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maximum) fail();
84
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
85
+ try {
86
+ const opened = fs.fstatSync(fd);
87
+ if (opened.dev !== before.dev || opened.ino !== before.ino) fail();
88
+ const bytes = Buffer.alloc(before.size + 1), length = fs.readSync(fd, bytes, 0, bytes.length, 0);
89
+ const after = fs.fstatSync(fd);
90
+ if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) fail();
91
+ return bytes.subarray(0, length);
92
+ } finally { fs.closeSync(fd); }
93
+ };
94
+ const metadata = JSON.parse(regular(manifestPath, 16384).toString('utf8'));
95
+ if (metadata.schemaVersion !== 1 || metadata.platform !== process.platform || metadata.architecture !== process.arch
96
+ || !/^[a-f0-9]{64}$/.test(metadata.sourceDigest) || !/^[a-f0-9]{64}$/.test(metadata.artifactDigest)
97
+ || metadata.package?.name !== '@agilno/rivet') fail();
98
+ const entries = []; let total = 0;
99
+ const visit = (directory, relative = '') => {
100
+ for (const name of fs.readdirSync(directory).sort()) {
101
+ if (name === '.rivet-runtime.json' && !relative) continue;
102
+ if (!name || /[\u0000-\u001f\u007f\\]/.test(name)) fail();
103
+ const ref = relative ? relative + '/' + name : name, file = path.join(directory, name), stat = fs.lstatSync(file);
104
+ if (entries.length >= 20000) fail();
105
+ if (stat.isSymbolicLink()) {
106
+ if (!ref.startsWith('node_modules/') || !inside(fs.realpathSync(file))) fail();
107
+ entries.push({ path: ref, kind: 'link', target: fs.readlinkSync(file) });
108
+ } else if (stat.isDirectory()) { entries.push({ path: ref, kind: 'directory' }); visit(file, ref); }
109
+ else {
110
+ const bytes = regular(file, 16 * 1024 * 1024); total += bytes.length;
111
+ if (total > 128 * 1024 * 1024) fail();
112
+ entries.push({ path: ref, kind: 'file', executable: Boolean(stat.mode & 0o111), sha256: digest(bytes) });
113
+ }
114
+ }
115
+ };
116
+ visit(root);
117
+ const id = digest(JSON.stringify({ metadata, entries }));
118
+ if (expectedId !== undefined && id !== expectedId) fail();
119
+ const entry = path.join(root, 'node_modules', '@agilno', 'rivet', 'bin', 'cli.js');
120
+ if (!inside(fs.realpathSync(entry))) fail();
121
+ const pkg = JSON.parse(regular(path.join(root, 'node_modules', '@agilno', 'rivet', 'package.json'), 65536));
122
+ if (pkg.name !== metadata.package.name || pkg.version !== metadata.package.version || !Array.isArray(pkg.files)) fail();
123
+ // Bind the installed Rivet code to the portable project source pin. Runtime
124
+ // dependencies and platform remain independently pinned in the private cache.
125
+ const packageRoot = path.join(root, 'node_modules', '@agilno', 'rivet');
126
+ const sourceFiles = new Map();
127
+ const sourceVisit = ref => {
128
+ if (typeof ref !== 'string' || !ref || path.isAbsolute(ref) || /[\u0000-\u001f\u007f\\*?]/.test(ref)
129
+ || ref.split('/').some(part => !part || part === '.' || part === '..' || part === '.git' || part === 'node_modules')) fail();
130
+ const file = path.join(packageRoot, ref), stat = fs.lstatSync(file);
131
+ if (stat.isSymbolicLink()) fail();
132
+ if (stat.isDirectory()) { for (const name of fs.readdirSync(file).sort()) sourceVisit(ref + '/' + name); return; }
133
+ sourceFiles.set(ref, { path: ref, mode: stat.mode & 0o111 ? 0o755 : 0o644, sha256: digest(regular(file,16*1024*1024)) });
134
+ };
135
+ sourceVisit('package.json');
136
+ for (const ref of pkg.files) sourceVisit(ref.replace(/\/$/, ''));
137
+ const sourceDigest = digest(JSON.stringify([...sourceFiles.values()].sort((a,b)=>Buffer.compare(Buffer.from(a.path),Buffer.from(b.path)))));
138
+ if (sourceDigest !== metadata.sourceDigest || (expectedSource !== undefined && sourceDigest !== expectedSource)) fail();
139
+ return { id, entry, metadata };
140
+ };
141
+ module.exports = { runtimeIntegrity, legacyRuntimeIntegrity };
@@ -0,0 +1,69 @@
1
+ export class IntegrationError extends Error {
2
+ constructor(reason = 'INPUT') {
3
+ super(`Integration ${reason.toLowerCase().replaceAll('_', ' ')}. Check project scope, provider selection, authentication and tool inventory.`);
4
+ this.name = 'IntegrationError';
5
+ this.code = `ERR_INTEGRATION_${reason}`;
6
+ this.safeMessage = this.message;
7
+ }
8
+ }
9
+ export function fail(reason) { throw new IntegrationError(reason); }
10
+ export function snapshot(value, maxBytes = 65536) {
11
+ // Accept plain JSON data only. Never invoke getters or toJSON methods.
12
+ let nodes = 0;
13
+ const walk = (item, depth = 0) => {
14
+ if (++nodes > 10000 || depth > 20) fail('INPUT');
15
+ if (item === null || typeof item === 'boolean' || typeof item === 'string') return item;
16
+ if (typeof item === 'number' && Number.isFinite(item)) return item;
17
+ if (!item || typeof item !== 'object' || ![Object.prototype, Array.prototype, null].includes(Object.getPrototypeOf(item))) fail('INPUT');
18
+ const array = Array.isArray(item);
19
+ if (array && (item.length > 10000 || Object.keys(item).length !== item.length
20
+ || Object.keys(item).some((key,index) => key !== String(index)))) fail('INPUT');
21
+ const result = array ? [] : Object.create(null);
22
+ for (const key of Reflect.ownKeys(item)) {
23
+ if (Array.isArray(item) && key === 'length') continue;
24
+ if (typeof key !== 'string' || ['__proto__','constructor','prototype'].includes(key)) fail('INPUT');
25
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
26
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor,'value')) fail('INPUT');
27
+ result[key] = walk(descriptor.value, depth + 1);
28
+ }
29
+ return result;
30
+ };
31
+ try {
32
+ const result = walk(value);
33
+ if (Buffer.byteLength(JSON.stringify(result)) > maxBytes) fail('INPUT');
34
+ return result;
35
+ } catch { fail('INPUT'); }
36
+ }
37
+ export function fields(value, allowed, required = allowed) {
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)
39
+ || Object.keys(value).some(key => !allowed.includes(key)) || required.some(key => !Object.hasOwn(value,key))) fail('INPUT');
40
+ }
41
+ export function safeUrl(value, { endpoint = false } = {}) {
42
+ try {
43
+ if (typeof value !== 'string' || value.length > 2048 || /[\s\u0000-\u001f]/.test(value)) fail('INPUT');
44
+ const url = new URL(value);
45
+ if (!['https:','http:'].includes(url.protocol) || url.username || url.password || url.hash) fail('INPUT');
46
+ const secretKey = /(?:password|passwd|secret|api[-_]?key|authorization|cookie|credential|access[-_]?token|api[-_]?token|token)/i;
47
+ const secretValue = /^(?:bearer\s+|basic\s+|gh[pousr]_|github_pat_|sk-[A-Za-z0-9]|xox[baprs]-|AKIA[0-9A-Z]{16})/i;
48
+ for (const [key, value] of url.searchParams) {
49
+ if (secretKey.test(key) || secretValue.test(value)) fail('INPUT');
50
+ if (!endpoint && (!['node-id','pageId'].includes(key) || !/^[A-Za-z0-9:_-]{1,128}$/.test(value))) fail('INPUT');
51
+ }
52
+ if (!endpoint && url.protocol !== 'https:') fail('INPUT');
53
+ let path = url.pathname;
54
+ for (let pass = 0; pass < 4; pass += 1) {
55
+ const decoded = decodeURIComponent(path);
56
+ if (decoded === path) break;
57
+ path = decoded;
58
+ }
59
+ if (/%[0-9a-f]{2}/i.test(path)) fail('INPUT');
60
+ for (const component of path.split('/')) {
61
+ if (secretValue.test(component) || (/[=:]/.test(component) && secretKey.test(component.split(/[=:]/)[0]))) fail('INPUT');
62
+ }
63
+ return url;
64
+ } catch { fail('INPUT'); }
65
+ }
66
+ export function negotiateCapabilities(provider, inventory) {
67
+ const tools = provider.tools ?? [];
68
+ return tools.filter(tool => inventory?.tools.includes(tool));
69
+ }
@@ -0,0 +1,26 @@
1
+ import { containsSecretMaterial } from '../clients/contract.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { fail, fields, safeUrl, snapshot } from './capabilities.js';
4
+ import { deepFreeze } from '../config/defaults.js';
5
+ function canonical(value) {
6
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
7
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
8
+ return JSON.stringify(value);
9
+ }
10
+ export function normalizeHostObservation(input, { provider, projectId }) {
11
+ const value = snapshot(input);
12
+ if (containsSecretMaterial(JSON.stringify(value))) fail('INPUT');
13
+ fields(value,['schemaVersion','providerId','projectId','tool','resourceId','sourceUrl','revision','capturedAt','content']);
14
+ if (value.schemaVersion !== 1 || value.providerId !== provider.id || value.projectId !== projectId
15
+ || provider.transport !== 'harness-mcp' || provider.mode === 'disabled'
16
+ || (provider.projectIds?.length && !provider.projectIds.includes(projectId))
17
+ || !provider.tools?.includes(value.tool)
18
+ || (provider.resourceIds?.length && !provider.resourceIds.includes(value.resourceId))) fail('INPUT');
19
+ for (const key of ['resourceId','revision','capturedAt']) {
20
+ if (typeof value[key] !== 'string' || !value[key].length || value[key].length > 256 || /[\u0000-\u001f]/.test(value[key])) fail('INPUT');
21
+ }
22
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{3})?Z$/.test(value.capturedAt) || !Number.isFinite(Date.parse(value.capturedAt))) fail('INPUT');
23
+ const url = safeUrl(value.sourceUrl);
24
+ if (provider.endpoint && safeUrl(provider.endpoint, {endpoint:true}).origin !== url.origin) fail('INPUT');
25
+ return deepFreeze({...value,provider:provider.kind,transport:'harness-mcp',assurance:'harness-observed',contentDigest:createHash('sha256').update(canonical(value.content)).digest('hex')});
26
+ }
@@ -0,0 +1,56 @@
1
+ import Ajv from 'ajv';
2
+ import { readFileSync } from 'node:fs';
3
+ import { deepFreeze } from '../config/defaults.js';
4
+ import { fail, fields, negotiateCapabilities, safeUrl, snapshot } from './capabilities.js';
5
+ const validate = new Ajv({strict:true}).compile(JSON.parse(readFileSync(new URL('../../schemas/integration.schema.json',import.meta.url))));
6
+ export function createIntegrationRegistry({config,projectId,environment = {},host}) {
7
+ if (typeof projectId !== 'string' || !projectId || projectId.length > 200) fail('INPUT');
8
+ const providers = snapshot(config?.providers?.providers,262144);
9
+ if (!Array.isArray(providers) || providers.length > 64 || new Set(providers.map(p=>p.id)).size !== providers.length) fail('INPUT');
10
+ let inventory;
11
+ if (host !== undefined) {
12
+ inventory = snapshot(host);
13
+ fields(inventory,['projectId','providers']);
14
+ if (inventory.projectId !== projectId || !Array.isArray(inventory.providers) || inventory.providers.length > 64) fail('INPUT');
15
+ const ids = new Set();
16
+ for (const item of inventory.providers) {
17
+ fields(item,['id','authenticated','tools']);
18
+ if (!providers.some(p=>p.id===item.id) || ids.has(item.id) || typeof item.authenticated !== 'boolean'
19
+ || !Array.isArray(item.tools) || item.tools.length > 64 || item.tools.some(t=>typeof t!=='string'|| !/^[A-Za-z0-9_.:-]{1,128}$/.test(t))) fail('INPUT');
20
+ ids.add(item.id);
21
+ }
22
+ }
23
+ const rows = providers.map(raw => {
24
+ if (!validate(raw)) fail('INPUT');
25
+ const provider = {...raw,transport:raw.transport ?? 'direct-api'};
26
+ if (provider.endpoint) safeUrl(provider.endpoint, {endpoint:true});
27
+ const observed = inventory?.providers.find(item=>item.id===provider.id);
28
+ const tools = negotiateCapabilities(provider,observed);
29
+ let readiness; let remedy;
30
+ if (provider.mode === 'disabled') {readiness='disabled';remedy='Enable this integration only when needed.';}
31
+ else if (provider.projectIds?.length && !provider.projectIds.includes(projectId)) {readiness='out-of-scope';remedy='Select an integration configured for this project.';}
32
+ else if (provider.transport === 'local-cli') {readiness='unsupported-transport';remedy='Local CLI descriptors are not executable yet.';}
33
+ else if (provider.transport === 'harness-mcp') {
34
+ readiness = !observed ? 'host-unavailable' : !observed.authenticated ? 'authentication-unavailable' : !tools.length ? 'unsupported-tools' : 'host-observed';
35
+ remedy = readiness === 'host-observed' ? 'Host-reported access; not independently verified.' : 'Supply current project-scoped authenticated host inventory with configured tools.';
36
+ } else {
37
+ const refs=Object.values(provider.credentials ?? {});
38
+ const present=refs.length > 0 && refs.every(ref=>{
39
+ const value=Object.getOwnPropertyDescriptor(environment,ref)?.value;
40
+ return typeof value === 'string' && value.length > 0 && value.length <= 8192 && !/[\u0000\r\n]/.test(value);
41
+ });
42
+ readiness=present ? 'credentials-present' : 'authentication-unavailable';
43
+ remedy=present ? 'Credentials are present; network access has not been checked.' : 'Configure referenced environment credentials when this integration is needed.';
44
+ }
45
+ return deepFreeze({...provider,availableTools:tools,readiness,remedy,assurance:provider.transport === 'harness-mcp' && observed ? 'harness-observed' : 'configured'});
46
+ });
47
+ return Object.freeze({list:()=>rows.slice(),check:()=>rows.slice(),resolve(options) {
48
+ const query=snapshot(options); fields(query,['providerId','kind','capability','tool'],['capability']);
49
+ const matches=rows.filter(row=>(!query.providerId||row.id===query.providerId)&&(!query.kind||row.kind===query.kind)&&row.capabilities.includes(query.capability));
50
+ if(matches.length>1) fail('AMBIGUOUS');
51
+ if(matches.length!==1) fail('NOT_FOUND');
52
+ const selected=matches[0];
53
+ if(!['host-observed','credentials-present'].includes(selected.readiness) || (query.tool && !selected.availableTools.includes(query.tool))) fail('UNAVAILABLE');
54
+ return selected;
55
+ }});
56
+ }
@@ -0,0 +1,102 @@
1
+ import { createModelRegistry } from './registry.js';
2
+ import { buildModelRequest, parseModelResponse } from './protocols.js';
3
+ import { requestModelJson } from './transport.js';
4
+ import { containsSecretMaterial } from '../clients/contract.js';
5
+ const IMPLEMENTED = new Set(['anthropic', 'openai', 'gemini', 'ollama', 'openai-compatible']);
6
+ const MESSAGES = Object.freeze({
7
+ 'invalid-input': 'Invalid text delegation input.',
8
+ 'unsupported-provider': 'This provider does not support direct text delegation.',
9
+ 'unsupported-budget': 'A monetary limit cannot be enforced for this model profile. Remove maxCostUsd or use another workflow.',
10
+ 'unsupported-timeout': 'Direct text delegation requires timeoutMs at most 120000.',
11
+ 'sensitive-prompt': 'The prompt contains credential-shaped text. Remove secrets before delegation.',
12
+ 'credential-unavailable': 'The configured model credential is missing or invalid.',
13
+ 'aborted': 'Text delegation was cancelled.',
14
+ 'timeout': 'Text delegation exceeded its deadline.',
15
+ 'request-failed': 'The model request failed or returned an unsupported response. No fallback was attempted.',
16
+ });
17
+ export class ModelDelegationError extends Error {
18
+ constructor(reason) {
19
+ super(MESSAGES[reason] ?? MESSAGES['request-failed']);
20
+ this.name = 'ModelDelegationError'; this.reason = reason;
21
+ this.code = `ERR_MODEL_DELEGATION_${reason.replaceAll('-', '_').toUpperCase()}`;
22
+ this.safeMessage = this.message;
23
+ }
24
+ }
25
+ const fail = reason => { throw new ModelDelegationError(reason); };
26
+ const ABORTED = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get;
27
+ const ADD = EventTarget.prototype.addEventListener, REMOVE = EventTarget.prototype.removeEventListener;
28
+ function capture(input) {
29
+ if (!input || typeof input !== 'object' || Array.isArray(input)
30
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail('invalid-input');
31
+ const output = {};
32
+ for (const key of Reflect.ownKeys(input)) {
33
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
34
+ if (!['profile','prompt','environment','signal','transport'].includes(key)
35
+ || !descriptor?.enumerable || !Object.hasOwn(descriptor,'value')) fail('invalid-input');
36
+ output[key] = descriptor.value;
37
+ }
38
+ return output;
39
+ }
40
+
41
+ export async function delegateText(input) {
42
+ let config, profile;
43
+ try {
44
+ config = capture(input);
45
+ profile = createModelRegistry().resolve(config.profile, { requiredCapabilities: ['text'] }).profile;
46
+ } catch { fail('invalid-input'); }
47
+ if (!IMPLEMENTED.has(profile.provider)) fail('unsupported-provider');
48
+ if (profile.maxCostUsd !== undefined) fail('unsupported-budget');
49
+ if (profile.timeoutMs > 120000) fail('unsupported-timeout');
50
+ if (typeof config.prompt !== 'string' || !config.prompt.trim() || config.prompt.includes('\0')
51
+ || Buffer.byteLength(config.prompt, 'utf8') > 64 * 1024) fail('invalid-input');
52
+ if (containsSecretMaterial(config.prompt)) fail('sensitive-prompt');
53
+ const environment = config.environment;
54
+ if (!environment || typeof environment !== 'object' || Array.isArray(environment)) fail('invalid-input');
55
+ let credential;
56
+ try {
57
+ if (profile.credentialEnv !== undefined) {
58
+ const value = Object.getOwnPropertyDescriptor(environment, profile.credentialEnv);
59
+ if (!value || !Object.hasOwn(value, 'value') || typeof value.value !== 'string'
60
+ || !/^[!-~]{1,4096}$/.test(value.value)) fail('credential-unavailable');
61
+ credential = value.value;
62
+ } else if (['anthropic','openai','gemini'].includes(profile.provider)) fail('credential-unavailable');
63
+ } catch { fail('credential-unavailable'); }
64
+ if (credential?.length >= 8 && config.prompt.includes(credential)) fail('sensitive-prompt');
65
+ const controller = new AbortController();
66
+ let timedOut = false, timer;
67
+ const deadline = Date.now() + profile.timeoutMs;
68
+ const abort = () => controller.abort();
69
+ try {
70
+ if (config.signal !== undefined) {
71
+ try {
72
+ if (Reflect.apply(ABORTED, config.signal, [])) fail('aborted');
73
+ Reflect.apply(ADD, config.signal, ['abort', abort, { once: true }]);
74
+ if (Reflect.apply(ABORTED, config.signal, [])) abort();
75
+ } catch (error) { if (error instanceof ModelDelegationError) throw error; fail('invalid-input'); }
76
+ }
77
+ timer = setTimeout(() => { timedOut = true; controller.abort(); }, profile.timeoutMs);
78
+ const request = buildModelRequest(profile, config.prompt, credential);
79
+ if (Date.now() >= deadline) fail('timeout');
80
+ const data = await requestModelJson({ provider: profile.provider, request, timeoutMs: profile.timeoutMs,
81
+ signal: controller.signal, ...(config.transport === undefined ? {} : { transport: config.transport }) });
82
+ if (controller.signal.aborted) fail(timedOut ? 'timeout' : 'aborted');
83
+ const result = parseModelResponse(profile, data);
84
+ if (Date.now() >= deadline) fail('timeout');
85
+ if (credential?.length >= 8 && JSON.stringify(result).includes(credential)) fail('request-failed');
86
+ if (typeof result.text !== 'string' || Buffer.byteLength(result.text) > 256 * 1024
87
+ || !Number.isSafeInteger(result.usage?.inputTokens) || result.usage.inputTokens < 0
88
+ || !Number.isSafeInteger(result.usage?.outputTokens) || result.usage.outputTokens < 0
89
+ || result.usage.outputTokens > profile.maxOutputTokens) fail('request-failed');
90
+ return Object.freeze({ provider: profile.provider, requestedModel: profile.model, model: result.model,
91
+ text: result.text, usage: Object.freeze({ ...result.usage }), verified: false });
92
+ } catch (error) {
93
+ if (error instanceof ModelDelegationError) throw error;
94
+ if (controller.signal.aborted) fail(timedOut ? 'timeout' : 'aborted');
95
+ fail('request-failed');
96
+ } finally {
97
+ clearTimeout(timer);
98
+ if (config.signal !== undefined) {
99
+ try { Reflect.apply(REMOVE, config.signal, ['abort', abort]); } catch {}
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,53 @@
1
+ import { createModelRegistry } from './registry.js';
2
+
3
+ const ID = /^[a-z][a-z0-9-]{0,63}$/;
4
+ function fail() { throw new TypeError('Invalid model role configuration.'); }
5
+ function record(input, allowed) {
6
+ if (!input || typeof input !== 'object' || Array.isArray(input)
7
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))) fail();
8
+ const keys = Reflect.ownKeys(input);
9
+ if (keys.length > 64) fail();
10
+ const result = Object.create(null);
11
+ for (const key of keys) {
12
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
13
+ if (typeof key !== 'string' || !ID.test(key) || ['constructor', 'prototype'].includes(key)
14
+ || (allowed && !allowed.includes(key)) || !descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail();
15
+ result[key] = descriptor.value;
16
+ }
17
+ return result;
18
+ }
19
+
20
+ // Explicit role selection is separate from automatic workflow-node routing.
21
+ // Unassigned roles stay with the caller's active harness and spawn nothing.
22
+ export function resolveRoleProfile(input, role) {
23
+ try {
24
+ if (typeof role !== 'string' || !ID.test(role)) fail();
25
+ // schemaVersion is the sole camel-case field in this versioned envelope.
26
+ const version = Object.getOwnPropertyDescriptor(input ?? {}, 'schemaVersion');
27
+ if (!version?.enumerable || !Object.hasOwn(version, 'value') || version.value !== 1) fail();
28
+ if (!input || ![Object.prototype, null].includes(Object.getPrototypeOf(input))
29
+ || Reflect.ownKeys(input).length !== 3) fail();
30
+ const profilesField = Object.getOwnPropertyDescriptor(input, 'profiles');
31
+ const rolesField = Object.getOwnPropertyDescriptor(input, 'roles');
32
+ if (!profilesField?.enumerable || !Object.hasOwn(profilesField, 'value')
33
+ || !rolesField?.enumerable || !Object.hasOwn(rolesField, 'value')) fail();
34
+ const registry = createModelRegistry();
35
+ const profiles = record(profilesField.value);
36
+ for (const key of Object.keys(profiles)) {
37
+ const resolved = registry.resolve(profiles[key], { requiredCapabilities: ['text'] });
38
+ if (resolved.provider.kind === 'harness') fail();
39
+ profiles[key] = resolved.profile;
40
+ }
41
+ const roles = record(rolesField.value);
42
+ for (const key of Object.keys(roles)) {
43
+ const target = record(roles[key], ['kind', 'profile', 'harness']);
44
+ if (target.kind === 'active-harness' && Object.keys(target).length === 1) roles[key] = Object.freeze({ kind: target.kind });
45
+ else if (target.kind === 'harness' && Object.keys(target).length === 2 && ['claude', 'codex'].includes(target.harness)) {
46
+ roles[key] = Object.freeze({ kind: target.kind, harness: target.harness });
47
+ } else if (target.kind === 'text' && Object.keys(target).length === 2 && typeof target.profile === 'string' && Object.hasOwn(profiles, target.profile)) {
48
+ roles[key] = Object.freeze({ kind: target.kind, profileId: target.profile, profile: profiles[target.profile] });
49
+ } else fail();
50
+ }
51
+ return Object.freeze({ role, target: roles[role] ?? Object.freeze({ kind: 'active-harness' }) });
52
+ } catch { fail(); }
53
+ }