@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,560 @@
|
|
|
1
|
+
import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import Ajv from 'ajv';
|
|
5
|
+
|
|
6
|
+
import { EVIDENCE_TYPES, SCHEMA_FILES } from './defaults.js';
|
|
7
|
+
import { CommandConfigurationError, compileProjectCommands, compileQualitySteps, compileProjectDependencies } from './commands.js';
|
|
8
|
+
|
|
9
|
+
const ajv = new Ajv({ allErrors: true, strict: true });
|
|
10
|
+
const validators = Object.fromEntries(
|
|
11
|
+
Object.entries(SCHEMA_FILES).map(([name, filename]) => {
|
|
12
|
+
const schema = JSON.parse(readFileSync(new URL(`../../schemas/${filename}`, import.meta.url), 'utf8'));
|
|
13
|
+
return [name, ajv.compile(schema)];
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const SAFE_SECRET_KEYS = new Set([
|
|
18
|
+
'credentials',
|
|
19
|
+
'apiTokenEnv',
|
|
20
|
+
'accessTokenEnv',
|
|
21
|
+
'tokenEnv',
|
|
22
|
+
'usernameEnv',
|
|
23
|
+
'tokenLimit',
|
|
24
|
+
]);
|
|
25
|
+
const SECRET_KEY = /(?:password|passwd|secret|api[-_]?key|authorization|cookie|credential|access[-_]?token|api[-_]?token|\btoken\b)/i;
|
|
26
|
+
const SECRET_VALUE = /^(?:bearer\s+|basic\s+|gh[pousr]_|github_pat_|sk-[A-Za-z0-9]|xox[baprs]-|AKIA[0-9A-Z]{16})/i;
|
|
27
|
+
const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
28
|
+
const EVIDENCE_TYPE_SET = new Set(EVIDENCE_TYPES);
|
|
29
|
+
const WINDOWS_RESERVED_PATH_COMPONENT = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
30
|
+
const ROLE_AUTHORITY_CEILINGS = Object.freeze({
|
|
31
|
+
boss: new Set(['plan', 'implement', 'delegate', 'verify', 'external-write', 'merge', 'deploy', 'change-authority', 'change-budget']),
|
|
32
|
+
manager: new Set(['implement', 'delegate', 'verify', 'external-write', 'merge']),
|
|
33
|
+
worker: new Set(['implement', 'verify']),
|
|
34
|
+
});
|
|
35
|
+
const ROLE_PERMISSION_CEILINGS = Object.freeze({
|
|
36
|
+
boss: new Set(['externalWrites', 'merge', 'deploy']),
|
|
37
|
+
manager: new Set(['externalWrites', 'merge']),
|
|
38
|
+
worker: new Set(),
|
|
39
|
+
});
|
|
40
|
+
const ROLE_DELEGATION_CEILINGS = Object.freeze({
|
|
41
|
+
boss: new Set(['manager']),
|
|
42
|
+
manager: new Set(['worker']),
|
|
43
|
+
worker: new Set(),
|
|
44
|
+
});
|
|
45
|
+
const HUMAN_FINAL_GATES = new Set(['publication', 'final-delivery']);
|
|
46
|
+
|
|
47
|
+
function safePath(path) {
|
|
48
|
+
if (!path || path === '/') return '/';
|
|
49
|
+
return path
|
|
50
|
+
.split('/')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map(segment => (/^[A-Za-z0-9_.-]{1,80}$/.test(segment) ? segment : '?'))
|
|
53
|
+
.join('/')
|
|
54
|
+
.replace(/^/, '/');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ConfigurationError extends Error {
|
|
58
|
+
constructor(path = '/', reason = 'invalid') {
|
|
59
|
+
super(`Tracked configuration is invalid at ${safePath(path)}.`);
|
|
60
|
+
this.name = 'ConfigurationError';
|
|
61
|
+
this.code = 'ERR_INVALID_TRACKED_CONFIG';
|
|
62
|
+
this.safeMessage = this.message;
|
|
63
|
+
this.details = Object.freeze({ path: safePath(path), reason });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(path, reason) {
|
|
68
|
+
throw new ConfigurationError(path, reason);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function schemaValidate(name, value) {
|
|
72
|
+
const validate = validators[name];
|
|
73
|
+
if (validate(value)) return;
|
|
74
|
+
const path = validate.errors?.[0]?.instancePath || '/';
|
|
75
|
+
fail(path, 'schema');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scanForSecrets(value, path = '') {
|
|
79
|
+
if (!value || typeof value !== 'object') return;
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
const childPath = `${path}/${key}`;
|
|
82
|
+
if (SECRET_KEY.test(key) && !SAFE_SECRET_KEYS.has(key)) fail(childPath, 'secret-key');
|
|
83
|
+
if (typeof child === 'string' && SECRET_VALUE.test(child)) fail(childPath, 'secret-value');
|
|
84
|
+
if (typeof child === 'string' && /^https?:\/\//i.test(child)) assertSafeUrl(child, childPath);
|
|
85
|
+
scanForSecrets(child, childPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function uniqueBy(items, key, path) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
const value = item[key];
|
|
93
|
+
if (seen.has(value)) fail(`${path}/${key}`, 'duplicate-id');
|
|
94
|
+
seen.add(value);
|
|
95
|
+
}
|
|
96
|
+
return seen;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertSafeUrl(value, path) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(value);
|
|
103
|
+
} catch {
|
|
104
|
+
fail(path, 'url');
|
|
105
|
+
}
|
|
106
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') fail(path, 'url-scheme');
|
|
107
|
+
if (parsed.username || parsed.password) fail(path, 'url-userinfo');
|
|
108
|
+
if (parsed.hash) fail(path, 'url-fragment');
|
|
109
|
+
let decodedPath = parsed.pathname;
|
|
110
|
+
let stabilized = false;
|
|
111
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
112
|
+
let nextPath;
|
|
113
|
+
try {
|
|
114
|
+
nextPath = decodeURIComponent(decodedPath);
|
|
115
|
+
} catch {
|
|
116
|
+
fail(path, 'url-path-encoding');
|
|
117
|
+
}
|
|
118
|
+
if (nextPath === decodedPath) {
|
|
119
|
+
stabilized = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
decodedPath = nextPath;
|
|
123
|
+
}
|
|
124
|
+
if (!stabilized) {
|
|
125
|
+
let nextPath;
|
|
126
|
+
try {
|
|
127
|
+
nextPath = decodeURIComponent(decodedPath);
|
|
128
|
+
} catch {
|
|
129
|
+
fail(path, 'url-path-encoding');
|
|
130
|
+
}
|
|
131
|
+
if (nextPath !== decodedPath) fail(path, 'url-path-encoding-depth');
|
|
132
|
+
}
|
|
133
|
+
if (/%[0-9a-f]{2}/i.test(decodedPath)) fail(path, 'url-path-encoding-depth');
|
|
134
|
+
for (const rawComponent of decodedPath.replace(/\\/g, '/').split('/')) {
|
|
135
|
+
const component = rawComponent.normalize('NFKC');
|
|
136
|
+
if (SECRET_VALUE.test(component)) fail(path, 'url-secret-path');
|
|
137
|
+
const delimiterIndex = component.search(/[=:]/);
|
|
138
|
+
if (delimiterIndex >= 0) {
|
|
139
|
+
const key = component.slice(0, delimiterIndex);
|
|
140
|
+
const componentValue = component.slice(delimiterIndex + 1);
|
|
141
|
+
if (SECRET_KEY.test(key) || SECRET_VALUE.test(componentValue)) fail(path, 'url-secret-path');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [key, queryValue] of parsed.searchParams) {
|
|
145
|
+
if (SECRET_KEY.test(key) || SECRET_VALUE.test(queryValue)) fail(path, 'url-secret-metadata');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertTimestamp(value, path) {
|
|
150
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?Z$/.exec(value);
|
|
151
|
+
if (!match) fail(path, 'timestamp');
|
|
152
|
+
const date = new Date(value);
|
|
153
|
+
const parts = match.slice(1, 7).map(Number);
|
|
154
|
+
if (
|
|
155
|
+
Number.isNaN(date.getTime())
|
|
156
|
+
|| date.getUTCFullYear() !== parts[0]
|
|
157
|
+
|| date.getUTCMonth() + 1 !== parts[1]
|
|
158
|
+
|| date.getUTCDate() !== parts[2]
|
|
159
|
+
|| date.getUTCHours() !== parts[3]
|
|
160
|
+
|| date.getUTCMinutes() !== parts[4]
|
|
161
|
+
|| date.getUTCSeconds() !== parts[5]
|
|
162
|
+
) fail(path, 'timestamp');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertSafeRelativePath(value, path) {
|
|
166
|
+
if (typeof value !== 'string' || value.length === 0) fail(path, 'unsafe-path');
|
|
167
|
+
const canonical = value.normalize('NFKC');
|
|
168
|
+
if (/[\u0000\r\n:]/.test(canonical)) fail(path, 'unsafe-path');
|
|
169
|
+
const normalized = canonical.replace(/\\/g, '/');
|
|
170
|
+
const segments = normalized.split('/');
|
|
171
|
+
if (
|
|
172
|
+
normalized === '.'
|
|
173
|
+
|| normalized.startsWith('/')
|
|
174
|
+
|| /^[A-Za-z]:/.test(canonical)
|
|
175
|
+
|| normalized.includes('//')
|
|
176
|
+
|| normalized.endsWith('/')
|
|
177
|
+
|| segments.some(segment => (
|
|
178
|
+
segment === ''
|
|
179
|
+
|| segment === '.'
|
|
180
|
+
|| segment === '..'
|
|
181
|
+
|| segment.endsWith('.')
|
|
182
|
+
|| segment.endsWith(' ')
|
|
183
|
+
|| WINDOWS_RESERVED_PATH_COMPONENT.test(segment)
|
|
184
|
+
))
|
|
185
|
+
) fail(path, 'unsafe-path');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateConfigSemantics(config) {
|
|
189
|
+
scanForSecrets(config);
|
|
190
|
+
|
|
191
|
+
const providerIds = uniqueBy(config.providers.providers, 'id', '/providers/providers');
|
|
192
|
+
if (config.project.deployment && !providerIds.has(config.project.deployment.providerId))
|
|
193
|
+
fail('/project/deployment/providerId', 'provider-reference');
|
|
194
|
+
for (const provider of config.providers.providers) {
|
|
195
|
+
if (provider.endpoint) assertSafeUrl(provider.endpoint, `/providers/providers/${provider.id}/endpoint`);
|
|
196
|
+
for (const [key, envName] of Object.entries(provider.credentials ?? {})) {
|
|
197
|
+
if (!key.endsWith('Env') || !ENV_NAME.test(envName)) fail(`/providers/providers/${provider.id}/credentials/${key}`, 'credential-reference');
|
|
198
|
+
}
|
|
199
|
+
for (const resourceId of provider.resourceIds ?? []) {
|
|
200
|
+
if (/^https?:\/\//i.test(resourceId)) assertSafeUrl(resourceId, `/providers/providers/${provider.id}/resourceIds`);
|
|
201
|
+
else assertSafeRelativePath(resourceId, `/providers/providers/${provider.id}/resourceIds`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try { compileProjectCommands(config.project); }
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof CommandConfigurationError) fail(error.path, error.reason);
|
|
208
|
+
fail('/project/commands', 'command');
|
|
209
|
+
}
|
|
210
|
+
for (const path of config.project.repository.sensitivePaths ?? []) assertSafeRelativePath(path, '/project/repository/sensitivePaths');
|
|
211
|
+
|
|
212
|
+
uniqueBy(config.orchestration.roles, 'id', '/orchestration/roles');
|
|
213
|
+
const profileIds = uniqueBy(config.orchestration.completionProfiles, 'id', '/orchestration/completionProfiles');
|
|
214
|
+
const profileMap = new Map(config.orchestration.completionProfiles.map(profile => [profile.id, profile]));
|
|
215
|
+
const kinds = new Set(config.orchestration.roles.map(role => role.kind));
|
|
216
|
+
for (const requiredKind of ['boss', 'manager', 'worker']) {
|
|
217
|
+
if (!kinds.has(requiredKind)) fail('/orchestration/roles', 'missing-role-kind');
|
|
218
|
+
}
|
|
219
|
+
const bossRoles = config.orchestration.roles.filter(role => role.kind === 'boss');
|
|
220
|
+
if (bossRoles.length !== 1) fail('/orchestration/roles', 'boss-count');
|
|
221
|
+
for (const role of config.orchestration.roles) {
|
|
222
|
+
if (role.harness !== undefined && (role.kind !== 'worker' || !['claude', 'codex'].includes(role.harness))) fail(`/orchestration/roles/${role.id}/harness`, 'worker-harness');
|
|
223
|
+
if (role.kind === 'worker' && role.canDelegate) fail(`/orchestration/roles/${role.id}/canDelegate`, 'worker-delegation');
|
|
224
|
+
if (role.canDelegate !== (role.delegatesTo.length > 0)) fail(`/orchestration/roles/${role.id}/delegatesTo`, 'delegation-policy');
|
|
225
|
+
for (const target of role.delegatesTo) {
|
|
226
|
+
if (!ROLE_DELEGATION_CEILINGS[role.kind].has(target)) fail(`/orchestration/roles/${role.id}/delegatesTo`, 'delegation-ceiling');
|
|
227
|
+
}
|
|
228
|
+
for (const scope of role.authorityScopes) {
|
|
229
|
+
if (!ROLE_AUTHORITY_CEILINGS[role.kind].has(scope)) fail(`/orchestration/roles/${role.id}/authorityScopes`, 'authority-ceiling');
|
|
230
|
+
}
|
|
231
|
+
for (const [permission, enabled] of Object.entries(role.permissions)) {
|
|
232
|
+
if (enabled && !ROLE_PERMISSION_CEILINGS[role.kind].has(permission)) {
|
|
233
|
+
fail(`/orchestration/roles/${role.id}/permissions/${permission}`, 'permission-ceiling');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!profileIds.has(role.completionProfile)) fail(`/orchestration/roles/${role.id}/completionProfile`, 'completion-profile-reference');
|
|
237
|
+
for (const providerRef of role.providerRefs) {
|
|
238
|
+
if (!providerIds.has(providerRef)) fail(`/orchestration/roles/${role.id}/providerRefs`, 'provider-reference');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const mandatoryGate of ['activation', 'external-write', 'final-delivery']) {
|
|
242
|
+
if (!config.orchestration.approvalGates.includes(mandatoryGate)) fail('/orchestration/approvalGates', 'mandatory-approval');
|
|
243
|
+
}
|
|
244
|
+
const bossProfile = profileMap.get(bossRoles[0].completionProfile);
|
|
245
|
+
if (!bossProfile.requiredApprovals.includes('human') || !bossProfile.requiredEvidence.includes('human-approval')) {
|
|
246
|
+
fail(`/orchestration/completionProfiles/${bossProfile.id}`, 'boss-human-approval');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const commandNames = new Set(Object.keys(config.project.commands));
|
|
250
|
+
const gateIds = uniqueBy(config.quality.commandGates, 'id', '/quality/commandGates');
|
|
251
|
+
for (const gate of config.quality.commandGates) {
|
|
252
|
+
if (!commandNames.has(gate.command)) fail(`/quality/commandGates/${gate.id}/command`, 'command-reference');
|
|
253
|
+
}
|
|
254
|
+
try { compileQualitySteps(config); compileProjectDependencies(config.project); }
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error instanceof CommandConfigurationError) fail(error.path, error.reason);
|
|
257
|
+
fail('/quality/commandGates', 'command-expansion');
|
|
258
|
+
}
|
|
259
|
+
if (config.project.schemaVersion === 3 && !config.quality.commandGates.some(gate =>
|
|
260
|
+
gate.required && ['test', 'check'].includes(gate.command))) fail('/quality/commandGates', 'mandatory-command-gate');
|
|
261
|
+
for (const mandatoryCommand of config.project.schemaVersion === 3 ? [] : ['build', 'test']) {
|
|
262
|
+
if (![...gateIds].some(id => config.quality.commandGates.some(gate => gate.id === id && gate.command === mandatoryCommand && gate.required))) {
|
|
263
|
+
fail('/quality/commandGates', 'mandatory-command-gate');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (config.quality.expectations.visual === 'human-baseline' && !config.quality.evidence.requireHumanBaseline) {
|
|
267
|
+
fail('/quality/evidence/requireHumanBaseline', 'human-baseline');
|
|
268
|
+
}
|
|
269
|
+
for (const providerRef of config.quality.providerRefs) {
|
|
270
|
+
if (!providerIds.has(providerRef)) fail('/quality/providerRefs', 'provider-reference');
|
|
271
|
+
}
|
|
272
|
+
for (const profileRef of config.quality.completionProfileRefs) {
|
|
273
|
+
if (!profileIds.has(profileRef)) fail('/quality/completionProfileRefs', 'completion-profile-reference');
|
|
274
|
+
}
|
|
275
|
+
const selectedProfiles = new Set(config.quality.completionProfileRefs);
|
|
276
|
+
const qualityEvidence = new Set(config.quality.evidence.requiredTypes);
|
|
277
|
+
for (const role of config.orchestration.roles) {
|
|
278
|
+
if (!selectedProfiles.has(role.completionProfile)) fail('/quality/completionProfileRefs', 'role-profile-selection');
|
|
279
|
+
for (const evidenceType of profileMap.get(role.completionProfile).requiredEvidence) {
|
|
280
|
+
if (!qualityEvidence.has(evidenceType)) fail('/quality/evidence/requiredTypes', 'profile-evidence-coverage');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const evidenceType of qualityEvidence) {
|
|
284
|
+
if (!EVIDENCE_TYPE_SET.has(evidenceType)) fail('/quality/evidence/requiredTypes', 'evidence-type');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function validateProjectConfiguration(config) {
|
|
289
|
+
for (const name of ['project', 'providers', 'orchestration', 'quality']) {
|
|
290
|
+
schemaValidate(name, config[name]);
|
|
291
|
+
}
|
|
292
|
+
if (config.project.repository.remote) {
|
|
293
|
+
try {
|
|
294
|
+
const identity = parseRepositoryRemote(config.project.repository.remote.url);
|
|
295
|
+
if (identity.url !== config.project.repository.remote.url) throw new Error();
|
|
296
|
+
} catch { throw new ConfigurationError('/project/repository/remote', 'invalid'); }
|
|
297
|
+
}
|
|
298
|
+
validateConfigSemantics(config);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function visitDependencies(nodeId, nodeMap, visited) {
|
|
303
|
+
if (visited.has(nodeId)) return;
|
|
304
|
+
const visiting = new Set([nodeId]);
|
|
305
|
+
const stack = [{ nodeId, dependencyIndex: 0 }];
|
|
306
|
+
while (stack.length > 0) {
|
|
307
|
+
const frame = stack[stack.length - 1];
|
|
308
|
+
const dependencies = nodeMap.get(frame.nodeId).dependencies;
|
|
309
|
+
if (frame.dependencyIndex >= dependencies.length) {
|
|
310
|
+
visiting.delete(frame.nodeId);
|
|
311
|
+
visited.add(frame.nodeId);
|
|
312
|
+
stack.pop();
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const dependency = dependencies[frame.dependencyIndex];
|
|
316
|
+
frame.dependencyIndex += 1;
|
|
317
|
+
if (visiting.has(dependency)) fail(`/nodes/${dependency}/dependencies`, 'dependency-cycle');
|
|
318
|
+
if (visited.has(dependency)) continue;
|
|
319
|
+
visiting.add(dependency);
|
|
320
|
+
stack.push({ nodeId: dependency, dependencyIndex: 0 });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function delegationDepth(nodeId, nodeMap, knownDepths) {
|
|
325
|
+
if (knownDepths.has(nodeId)) return knownDepths.get(nodeId);
|
|
326
|
+
const visiting = new Set();
|
|
327
|
+
const path = [];
|
|
328
|
+
let currentId = nodeId;
|
|
329
|
+
let depth;
|
|
330
|
+
while (true) {
|
|
331
|
+
if (knownDepths.has(currentId)) {
|
|
332
|
+
depth = knownDepths.get(currentId);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (visiting.has(currentId)) fail(`/nodes/${currentId}/parentId`, 'parent-cycle');
|
|
336
|
+
visiting.add(currentId);
|
|
337
|
+
path.push(currentId);
|
|
338
|
+
const parentId = nodeMap.get(currentId).parentId;
|
|
339
|
+
if (!parentId) {
|
|
340
|
+
depth = -1;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
currentId = parentId;
|
|
344
|
+
}
|
|
345
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
346
|
+
depth += 1;
|
|
347
|
+
knownDepths.set(path[index], depth);
|
|
348
|
+
}
|
|
349
|
+
return knownDepths.get(nodeId);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function validateGoalGraph(graph, config) {
|
|
353
|
+
schemaValidate('goalGraph', graph);
|
|
354
|
+
scanForSecrets(graph);
|
|
355
|
+
const nodeIds = uniqueBy(graph.nodes, 'id', '/nodes');
|
|
356
|
+
const nodeMap = new Map(graph.nodes.map(node => [node.id, node]));
|
|
357
|
+
for (const node of graph.nodes) {
|
|
358
|
+
for (const dependency of node.dependencies) {
|
|
359
|
+
if (!nodeIds.has(dependency)) fail(`/nodes/${node.id}/dependencies`, 'dangling-dependency');
|
|
360
|
+
if (dependency === node.id) fail(`/nodes/${node.id}/dependencies`, 'self-dependency');
|
|
361
|
+
}
|
|
362
|
+
if (node.parentId && !nodeIds.has(node.parentId)) fail(`/nodes/${node.id}/parentId`, 'dangling-parent');
|
|
363
|
+
if (node.parentId === node.id) fail(`/nodes/${node.id}/parentId`, 'self-parent');
|
|
364
|
+
}
|
|
365
|
+
const roots = graph.nodes.filter(node => !node.parentId);
|
|
366
|
+
if (roots.length !== 1 || roots[0].owner.role !== 'boss') fail('/nodes', 'boss-root');
|
|
367
|
+
const finalDeliveryNodes = graph.nodes.filter(node => node.approvalGate === 'final-delivery');
|
|
368
|
+
if (finalDeliveryNodes.length !== 1) fail('/nodes', 'final-delivery-node');
|
|
369
|
+
for (const node of graph.nodes) {
|
|
370
|
+
if (node.approvalGate && HUMAN_FINAL_GATES.has(node.approvalGate)) {
|
|
371
|
+
if (node.owner.role !== 'boss' || !node.requiredEvidenceTypes.includes('human-approval')) {
|
|
372
|
+
fail(`/nodes/${node.id}/approvalGate`, 'human-final-gate');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (node.approvalGate && node.owner.role === 'worker') {
|
|
376
|
+
fail(`/nodes/${node.id}/approvalGate`, 'worker-approval-gate');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const visitedDependencies = new Set();
|
|
380
|
+
for (const node of graph.nodes) visitDependencies(node.id, nodeMap, visitedDependencies);
|
|
381
|
+
const knownDelegationDepths = new Map();
|
|
382
|
+
for (const node of graph.nodes) {
|
|
383
|
+
if (delegationDepth(node.id, nodeMap, knownDelegationDepths) > graph.maxDelegationDepth) fail(`/nodes/${node.id}/parentId`, 'delegation-depth');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (config) {
|
|
387
|
+
if (graph.maxDelegationDepth > config.orchestration.maxDelegationDepth) fail('/maxDelegationDepth', 'configured-delegation-depth');
|
|
388
|
+
const providerIds = new Set(config.providers.providers.map(provider => provider.id));
|
|
389
|
+
const roleMap = new Map(config.orchestration.roles.map(role => [role.id, role]));
|
|
390
|
+
const profileIds = new Set(config.orchestration.completionProfiles.map(profile => profile.id));
|
|
391
|
+
const profileMap = new Map(config.orchestration.completionProfiles.map(profile => [profile.id, profile]));
|
|
392
|
+
const rootProfile = profileMap.get(roots[0].completionProfile);
|
|
393
|
+
if (!rootProfile || !rootProfile.requiredApprovals.includes('human') || !rootProfile.requiredEvidence.includes('human-approval')) {
|
|
394
|
+
fail(`/nodes/${roots[0].id}/completionProfile`, 'boss-root-human-approval');
|
|
395
|
+
}
|
|
396
|
+
for (const providerRef of graph.providerRefs) {
|
|
397
|
+
if (!providerIds.has(providerRef)) fail('/providerRefs', 'provider-reference');
|
|
398
|
+
}
|
|
399
|
+
for (const node of graph.nodes) {
|
|
400
|
+
const role = roleMap.get(node.owner.id);
|
|
401
|
+
if (!role || role.kind !== node.owner.role) fail(`/nodes/${node.id}/owner`, 'role-reference');
|
|
402
|
+
if (!profileIds.has(node.completionProfile)) fail(`/nodes/${node.id}/completionProfile`, 'completion-profile-reference');
|
|
403
|
+
if (node.completionProfile !== role.completionProfile) fail(`/nodes/${node.id}/completionProfile`, 'completion-profile-bound');
|
|
404
|
+
if (node.approvalGate && !config.orchestration.approvalGates.includes(node.approvalGate)) fail(`/nodes/${node.id}/approvalGate`, 'approval-reference');
|
|
405
|
+
for (const scope of node.authorityScopes) {
|
|
406
|
+
if (!role.authorityScopes.includes(scope)) fail(`/nodes/${node.id}/authorityScopes`, 'authority-bound');
|
|
407
|
+
}
|
|
408
|
+
for (const budgetName of ['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']) {
|
|
409
|
+
if (node.budget[budgetName] > role.budget[budgetName]) fail(`/nodes/${node.id}/budget/${budgetName}`, 'budget-bound');
|
|
410
|
+
}
|
|
411
|
+
const profile = profileMap.get(node.completionProfile);
|
|
412
|
+
if (node.approvalGate && HUMAN_FINAL_GATES.has(node.approvalGate)) {
|
|
413
|
+
if (!profile.requiredApprovals.includes('human') || !profile.requiredEvidence.includes('human-approval')) {
|
|
414
|
+
fail(`/nodes/${node.id}/completionProfile`, 'human-final-profile');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const evidenceType of profile.requiredEvidence) {
|
|
418
|
+
if (!node.requiredEvidenceTypes.includes(evidenceType)) fail(`/nodes/${node.id}/requiredEvidenceTypes`, 'completion-evidence-coverage');
|
|
419
|
+
}
|
|
420
|
+
for (const evidenceType of node.requiredEvidenceTypes) {
|
|
421
|
+
if (!config.quality.evidence.requiredTypes.includes(evidenceType)) fail(`/nodes/${node.id}/requiredEvidenceTypes`, 'quality-evidence-coverage');
|
|
422
|
+
}
|
|
423
|
+
if (node.parentId) {
|
|
424
|
+
const parent = nodeMap.get(node.parentId);
|
|
425
|
+
const parentRole = roleMap.get(parent.owner.id);
|
|
426
|
+
if (node.owner.role === 'boss') {
|
|
427
|
+
const isBoundFinalGate = (
|
|
428
|
+
parent.id === roots[0].id
|
|
429
|
+
&& parent.owner.role === 'boss'
|
|
430
|
+
&& parent.owner.id === node.owner.id
|
|
431
|
+
&& HUMAN_FINAL_GATES.has(node.approvalGate)
|
|
432
|
+
&& node.completionProfile === parent.completionProfile
|
|
433
|
+
&& node.authorityScopes.every(scope => scope === 'verify')
|
|
434
|
+
);
|
|
435
|
+
if (!isBoundFinalGate) fail(`/nodes/${node.id}/parentId`, 'non-root-boss');
|
|
436
|
+
} else {
|
|
437
|
+
if (!parentRole.canDelegate) fail(`/nodes/${node.id}/parentId`, 'parent-cannot-delegate');
|
|
438
|
+
if (!parentRole.delegatesTo.includes(node.owner.role)) fail(`/nodes/${node.id}/owner`, 'delegation-target');
|
|
439
|
+
}
|
|
440
|
+
for (const scope of node.authorityScopes) {
|
|
441
|
+
if (!parent.authorityScopes.includes(scope)) fail(`/nodes/${node.id}/authorityScopes`, 'parent-authority-bound');
|
|
442
|
+
}
|
|
443
|
+
for (const budgetName of ['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']) {
|
|
444
|
+
if (node.budget[budgetName] > parent.budget[budgetName]) fail(`/nodes/${node.id}/budget/${budgetName}`, 'parent-budget-bound');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function snapshotEventRecord(value, maximumKeys, path) {
|
|
453
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(path, 'event-snapshot');
|
|
454
|
+
const keys = Reflect.ownKeys(value);
|
|
455
|
+
if (keys.length > maximumKeys || keys.some(key => typeof key !== 'string' || key === '__proto__')) fail(path, 'event-snapshot');
|
|
456
|
+
const output = Object.create(null);
|
|
457
|
+
for (const key of keys) {
|
|
458
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
459
|
+
if (!descriptor?.enumerable) fail(path, 'event-snapshot');
|
|
460
|
+
output[key] = value[key];
|
|
461
|
+
}
|
|
462
|
+
return output;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function snapshotEventArray(value, maximumItems, path) {
|
|
466
|
+
if (!Array.isArray(value)) fail(path, 'event-snapshot');
|
|
467
|
+
const length = value.length;
|
|
468
|
+
if (!Number.isSafeInteger(length) || length > maximumItems) fail(path, 'event-snapshot');
|
|
469
|
+
const output = [];
|
|
470
|
+
for (let index = 0; index < length; index += 1) {
|
|
471
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
472
|
+
if (!descriptor?.enumerable) fail(path, 'event-snapshot');
|
|
473
|
+
output.push(value[index]);
|
|
474
|
+
}
|
|
475
|
+
return output;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function snapshotEventInput(event) {
|
|
479
|
+
try {
|
|
480
|
+
const stable = snapshotEventRecord(event, 32, '/');
|
|
481
|
+
if (Object.hasOwn(stable, 'actor')) stable.actor = snapshotEventRecord(stable.actor, 4, '/actor');
|
|
482
|
+
if (Object.hasOwn(stable, 'evidenceRefs')) stable.evidenceRefs = snapshotEventArray(stable.evidenceRefs, 64, '/evidenceRefs');
|
|
483
|
+
if (Object.hasOwn(stable, 'authorityDelta')) {
|
|
484
|
+
stable.authorityDelta = snapshotEventRecord(stable.authorityDelta, 2, '/authorityDelta');
|
|
485
|
+
if (Object.hasOwn(stable.authorityDelta, 'added')) stable.authorityDelta.added = snapshotEventArray(stable.authorityDelta.added, 32, '/authorityDelta/added');
|
|
486
|
+
if (Object.hasOwn(stable.authorityDelta, 'removed')) stable.authorityDelta.removed = snapshotEventArray(stable.authorityDelta.removed, 32, '/authorityDelta/removed');
|
|
487
|
+
}
|
|
488
|
+
if (Object.hasOwn(stable, 'budgetDelta')) stable.budgetDelta = snapshotEventRecord(stable.budgetDelta, 4, '/budgetDelta');
|
|
489
|
+
return stable;
|
|
490
|
+
} catch {
|
|
491
|
+
fail('/', 'event-snapshot');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function validateEvent(event) {
|
|
496
|
+
const stable = snapshotEventInput(event);
|
|
497
|
+
schemaValidate('event', stable);
|
|
498
|
+
scanForSecrets(stable);
|
|
499
|
+
assertTimestamp(stable.timestamp, '/timestamp');
|
|
500
|
+
if (stable.type === 'approval-recorded') {
|
|
501
|
+
if (stable.actor.role !== 'human') fail('/actor/role', 'approval-human-actor');
|
|
502
|
+
if (!stable.evidenceRefs.includes(stable.approvalReceiptId)) fail('/approvalReceiptId', 'approval-receipt-reference');
|
|
503
|
+
}
|
|
504
|
+
if (stable.priorState && stable.newState && stable.priorState === stable.newState) fail('/newState', 'unchanged-state');
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function validateEvidence(evidence, config) {
|
|
509
|
+
schemaValidate('evidence', evidence);
|
|
510
|
+
scanForSecrets(evidence);
|
|
511
|
+
uniqueBy(evidence.items, 'id', '/items');
|
|
512
|
+
if (config) {
|
|
513
|
+
const providerIds = new Set(config.providers.providers.map(provider => provider.id));
|
|
514
|
+
for (const item of evidence.items) {
|
|
515
|
+
if (item.source.providerRef && !providerIds.has(item.source.providerRef)) fail(`/items/${item.id}/source/providerRef`, 'provider-reference');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const item of evidence.items) {
|
|
519
|
+
assertTimestamp(item.timestamp, `/items/${item.id}/timestamp`);
|
|
520
|
+
if (item.location && /^[A-Za-z][A-Za-z0-9+.-]*:/.test(item.location)) {
|
|
521
|
+
if (!/^https?:\/\//i.test(item.location)) fail(`/items/${item.id}/location`, 'location-scheme');
|
|
522
|
+
assertSafeUrl(item.location, `/items/${item.id}/location`);
|
|
523
|
+
} else if (item.location) assertSafeRelativePath(item.location, `/items/${item.id}/location`);
|
|
524
|
+
if (item.source.resourceId) {
|
|
525
|
+
const resourcePath = `/items/${item.id}/source/resourceId`;
|
|
526
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(item.source.resourceId)) {
|
|
527
|
+
if (!/^https?:\/\//i.test(item.source.resourceId)) fail(resourcePath, 'resource-scheme');
|
|
528
|
+
assertSafeUrl(item.source.resourceId, resourcePath);
|
|
529
|
+
} else {
|
|
530
|
+
assertSafeRelativePath(item.source.resourceId, resourcePath);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (item.type === 'human-approval') {
|
|
534
|
+
if (item.producer.role !== 'human') fail(`/items/${item.id}/producer/role`, 'human-approver');
|
|
535
|
+
if (item.approval.actorId !== item.producer.id) fail(`/items/${item.id}/approval/actorId`, 'approver-identity');
|
|
536
|
+
if (item.approval.decision !== item.approvalState) fail(`/items/${item.id}/approvalState`, 'approval-decision-state');
|
|
537
|
+
if (item.approval.decision !== evidence.approvalState) fail('/approvalState', 'bundle-decision-state');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const approvedHumanItems = evidence.items.filter(item => item.type === 'human-approval' && item.approval.decision === 'approved');
|
|
541
|
+
const rejectedHumanItems = evidence.items.filter(item => item.type === 'human-approval' && item.approval.decision === 'rejected');
|
|
542
|
+
if (evidence.approvalState === 'approved') {
|
|
543
|
+
if (approvedHumanItems.length === 0 || evidence.items.some(item => item.approvalState !== 'approved')) fail('/approvalState', 'approved-bundle-evidence');
|
|
544
|
+
}
|
|
545
|
+
if (evidence.approvalState === 'rejected' && rejectedHumanItems.length === 0) {
|
|
546
|
+
fail('/approvalState', 'rejected-bundle-evidence');
|
|
547
|
+
}
|
|
548
|
+
if (evidence.publicationState === 'approved' && (evidence.approvalState !== 'approved' || approvedHumanItems.length === 0)) {
|
|
549
|
+
fail('/publicationState', 'publication-approval');
|
|
550
|
+
}
|
|
551
|
+
if (evidence.publicationState !== 'approved' && evidence.items.some(item => item.classification === 'public')) {
|
|
552
|
+
fail('/publicationState', 'public-evidence-approval');
|
|
553
|
+
}
|
|
554
|
+
if (evidence.publicationState === 'rejected' && evidence.items.some(item => item.classification === 'public')) {
|
|
555
|
+
fail('/publicationState', 'rejected-publication');
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export const validateConfig = validateProjectConfiguration;
|