chati-dev 4.5.28 → 4.5.29
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/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { canonicalSerialize, sha256 } from '@chati/core';
|
|
5
|
+
import { validatePlanningMaterial, validateTaskVerificationPlan, validateTaskBrowserVerificationPlan } from '@chati/planning';
|
|
6
|
+
import { validateBrowserLaunchRequest } from '@chati/browser-capability';
|
|
7
|
+
import { browserEvidenceMaterial, completionMetricsReference, effortJournalDigest, providerProcessingMetric } from '@chati/rail';
|
|
8
|
+
import { delegatedAuthorityPublicIdentity, signDelegatedAuthorityStatement } from '../installer-v2/installation-authority.js';
|
|
9
|
+
import { persistImmutableInstallationFile, readSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
|
|
10
|
+
import { runIsolatedVerificationCheck } from '../terminal/spawner.js';
|
|
11
|
+
import { verificationProcessCompleted } from '../terminal/verification-process-facts.js';
|
|
12
|
+
import { runIsolatedBrowserObservation } from '../terminal/browser-observer.js';
|
|
13
|
+
import { browserObservationCompleted } from '../terminal/browser-process-facts.js';
|
|
14
|
+
import { assertRailReadOnlyCommitWorkspaceUnchanged, cleanupRailReadOnlyCommitWorkspace, createRailReadOnlyCommitWorkspace } from '../terminal/rail-readonly-workspace.js';
|
|
15
|
+
import { loadRuntimeInstallationV2, resolveRuntimeCouncilGatePolicyV2 } from './runtime-installation-v2.js';
|
|
16
|
+
import { assertCouncilGateApproved } from './council-gate-runtime.js';
|
|
17
|
+
import { assertRailHandoffCouncilBinding, collectRailProviderProcessing, resolveRailAttemptContext } from './rail-runtime.js';
|
|
18
|
+
import { collectRailHumanEffort } from './rail-human-effort.js';
|
|
19
|
+
import { resolveReviewGitSubject } from './review-runtime.js';
|
|
20
|
+
import { signedRailEvidencePayload, verifyRailEvidenceAuthorityCertificate, verifyRailEvidenceReceipt } from './rail-evidence-authority.js';
|
|
21
|
+
import { assertLocalBrowserArtifacts, browserRetentionPolicy, persistBrowserArtifact, pruneBrowserArtifacts } from './browser-artifact-store.js';
|
|
22
|
+
|
|
23
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
24
|
+
function timestamp(clock) {
|
|
25
|
+
const value = new Date(clock());
|
|
26
|
+
if (!Number.isFinite(value.getTime())) fail('RAIL_VERIFICATION_TIME_INVALID', 'verification clock must be valid');
|
|
27
|
+
return value.toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadCertifiedIdentity({ projectDir, artifact, authorityDirectory, clock }) {
|
|
31
|
+
const source = readSafeInstallationStorageFile(join(projectDir, '.chati/v2/rail/evidence-authority.json'), {
|
|
32
|
+
allowMissing: true, maxBytes: 32 * 1024, label: 'RAIL evidence certificate',
|
|
33
|
+
});
|
|
34
|
+
if (source === null) fail('RAIL_EVIDENCE_AUTHORITY_NOT_CONFIGURED', 'enroll an approved CHATI-signed evidence authority before running local verification');
|
|
35
|
+
let certificate;
|
|
36
|
+
try { certificate = JSON.parse(source.content); }
|
|
37
|
+
catch { fail('INVALID_RAIL_EVIDENCE_CERTIFICATE', 'RAIL evidence certificate is not valid JSON'); }
|
|
38
|
+
const verifier = verifyRailEvidenceAuthorityCertificate({ certificate,
|
|
39
|
+
expected_project_id: artifact.project_id,
|
|
40
|
+
root_public_key: readFileSync(new URL('../installer/signing-public-key.pem', import.meta.url), 'utf8'), clock });
|
|
41
|
+
const signingOptions = { projectDir, projectId: artifact.project_id, kind: 'rail-evidence', authorityDirectory };
|
|
42
|
+
const identity = delegatedAuthorityPublicIdentity(signingOptions);
|
|
43
|
+
if (certificate.issuer_id !== identity.issuer_id || certificate.public_key !== identity.public_key) {
|
|
44
|
+
fail('RAIL_VERIFICATION_ISSUER_MISMATCH', 'enrolled certificate does not certify this local delegated identity');
|
|
45
|
+
}
|
|
46
|
+
return { verifier, identity, signingOptions, certificate_digest: sha256(certificate) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canonicalContext(options) {
|
|
50
|
+
const { projectDir, handoff_id, attempt_id, clock } = options;
|
|
51
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
52
|
+
if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'local verification requires a verified v2 installation');
|
|
53
|
+
const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
|
|
54
|
+
if (context.handoff.project_id !== artifact.project_id) fail('RAIL_VERIFICATION_PROJECT_MISMATCH', 'handoff belongs to another installation');
|
|
55
|
+
if (!['active', 'ready_to_merge'].includes(context.state)) fail('RAIL_VERIFICATION_ATTEMPT_INACTIVE', `attempt is ${context.state}`);
|
|
56
|
+
const material = validatePlanningMaterial(context.handoff, { required: true });
|
|
57
|
+
const plan = validateTaskVerificationPlan(context.task, { required: true });
|
|
58
|
+
// Same council gate as RAIL dispatch: a guided (single-provider) installation
|
|
59
|
+
// must be reconfigured, while an installation whose contract predates the
|
|
60
|
+
// council policy keeps the dispatch semantics instead of a stricter gate.
|
|
61
|
+
const council = resolveRuntimeCouncilGatePolicyV2({ artifact, stage: 'execution-design' });
|
|
62
|
+
if (!council.enabled && council.reason === 'GUIDED_COUNCIL_REQUIRED') {
|
|
63
|
+
fail('GUIDED_COUNCIL_REQUIRED', 'local verification requires installation reconfiguration with two selected review providers');
|
|
64
|
+
}
|
|
65
|
+
const approval = council.enabled
|
|
66
|
+
? assertCouncilGateApproved({ projectDir, stage: 'execution-design', artifact,
|
|
67
|
+
policy: council.policy, handoff: context.handoff, clock })
|
|
68
|
+
: null;
|
|
69
|
+
if (approval) assertRailHandoffCouncilBinding({ projectDir, handoff: context.handoff, approval });
|
|
70
|
+
const subject = resolveReviewGitSubject({ projectDir, attempt_id, clock });
|
|
71
|
+
const authority = loadCertifiedIdentity({ ...options, artifact });
|
|
72
|
+
const browser = canonicalBrowserContext(context);
|
|
73
|
+
const effort = artifact.installation.external_integrations?.clickup === 'required' ? {
|
|
74
|
+
human: collectRailHumanEffort({ projectDir, attempt_id }),
|
|
75
|
+
processing: collectRailProviderProcessing({ projectDir, attempt_id, clock }),
|
|
76
|
+
source_journal_digest: effortJournalDigest(context.records, attempt_id),
|
|
77
|
+
rework_review_refs: context.records.filter((record) => record.type === 'review_decided'
|
|
78
|
+
&& record.payload.attempt_id === attempt_id && record.payload.decision?.decision === 'normal-rework')
|
|
79
|
+
.map((record) => record.payload.review_ref),
|
|
80
|
+
} : null;
|
|
81
|
+
return { context, plan, material, subject, authority, effort, browser,
|
|
82
|
+
binding_digest: sha256({ installation: artifact, subject, plan, browser, effort, certificate_digest: authority.certificate_digest }) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function canonicalBrowserContext(context) {
|
|
86
|
+
if (!context.task.requires_browser) return null;
|
|
87
|
+
const plan = validateTaskBrowserVerificationPlan(context.task, { required: true });
|
|
88
|
+
const policy = context.task.browser_policy;
|
|
89
|
+
browserRetentionPolicy(policy.artifact_policy.retention_class);
|
|
90
|
+
const manifest = [...context.records].reverse().find((record) => record.type === 'execution_dispatched'
|
|
91
|
+
&& record.payload.attempt_id === context.attempt.attempt_id && record.payload.run_kind === 'initial'
|
|
92
|
+
&& record.payload.browser_manifest)?.payload.browser_manifest;
|
|
93
|
+
const core = { ...manifest }; delete core.manifest_hash;
|
|
94
|
+
if (!manifest || manifest.manifest_type !== 'BrowserDelegationManifest' || manifest.schema_version !== 1
|
|
95
|
+
|| manifest.evidence_class !== 'delegation-only' || manifest.manifest_hash !== sha256(core)
|
|
96
|
+
|| manifest.delegation?.status !== 'delegated' || manifest.delegation?.evidence_status !== 'required'
|
|
97
|
+
|| manifest.task_id !== context.task.task_id || manifest.policy_ref !== policy.policy_id
|
|
98
|
+
|| manifest.mode !== policy.mode || manifest.tool !== policy.tool || manifest.profile_isolation !== policy.profile_isolation
|
|
99
|
+
|| manifest.allowed_domain !== plan.observation.hostname || manifest.action_confirmation !== policy.action_confirmation
|
|
100
|
+
|| sha256(manifest.artifact_policy) !== sha256(policy.artifact_policy)) {
|
|
101
|
+
fail('BROWSER_VERIFICATION_DELEGATION_INVALID', 'real observation requires the exact canonical delegation, approved tool policy and local scenario hostname');
|
|
102
|
+
}
|
|
103
|
+
validateBrowserLaunchRequest({ browser_session_id: manifest.browser_session_id, task_id: manifest.task_id,
|
|
104
|
+
url: `http://${manifest.allowed_domain}:${plan.observation.application.port}${plan.observation.steps[0].path}`,
|
|
105
|
+
classification: manifest.classification, ...(manifest.tenant_ref ? { tenant_ref: manifest.tenant_ref } : {}),
|
|
106
|
+
...(manifest.action_confirmation_ref ? { action_confirmation_ref: manifest.action_confirmation_ref } : {}) }, policy);
|
|
107
|
+
return { plan, policy, manifest };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function persistReport(projectDir, record) {
|
|
111
|
+
const digest = sha256(record);
|
|
112
|
+
const report_path = `.chati/v2/rail/verifications/${digest}.json`;
|
|
113
|
+
persistImmutableInstallationFile(join(projectDir, report_path), `${canonicalSerialize(record)}\n`, {
|
|
114
|
+
label: 'immutable RAIL verification report', conflictCode: 'RAIL_VERIFICATION_REPORT_CONFLICT',
|
|
115
|
+
});
|
|
116
|
+
return { report_path, report_ref: `rail-verification://sha256/${digest}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Canonical observer: no caller-selected command, commit, result or verdict. */
|
|
120
|
+
export async function observeRailVerification(input = {}) {
|
|
121
|
+
const allowed = ['projectDir', 'handoff_id', 'attempt_id', 'authorityDirectory', 'clock'];
|
|
122
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)
|
|
123
|
+
|| Object.keys(input).some((key) => !allowed.includes(key))
|
|
124
|
+
|| ['projectDir', 'handoff_id', 'attempt_id'].some((key) => typeof input[key] !== 'string' || !input[key].trim())
|
|
125
|
+
|| (input.clock !== undefined && typeof input.clock !== 'function')) {
|
|
126
|
+
fail('RAIL_VERIFICATION_INPUT_INVALID', 'verification accepts only the project, canonical handoff and attempt identifiers');
|
|
127
|
+
}
|
|
128
|
+
const options = { ...input, projectDir: resolve(input.projectDir), clock: input.clock ?? (() => new Date()) };
|
|
129
|
+
const initial = canonicalContext(options);
|
|
130
|
+
const scripts = new Map(initial.material.artifacts.map((entry) => [entry.ref, entry.content]));
|
|
131
|
+
const run_id = `verification-${randomUUID()}`;
|
|
132
|
+
const started_at = timestamp(options.clock);
|
|
133
|
+
const checks = [];
|
|
134
|
+
if (initial.browser) {
|
|
135
|
+
const cleanup = pruneBrowserArtifacts({ projectDir: options.projectDir, projectId: initial.context.handoff.project_id, clock: options.clock });
|
|
136
|
+
if (!cleanup.ok) fail('BROWSER_ARTIFACT_PRUNE_FAILED', `browser store has unresolved collection failures: ${cleanup.failures.map((item) => item.code).join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
for (const check of initial.plan.checks) {
|
|
139
|
+
let workspace;
|
|
140
|
+
let observed;
|
|
141
|
+
let errorCode = null;
|
|
142
|
+
let completionProven = false;
|
|
143
|
+
try {
|
|
144
|
+
workspace = createRailReadOnlyCommitWorkspace({ projectDir: options.projectDir,
|
|
145
|
+
baseCommitRef: initial.subject.base_commit_ref, resultCommitRef: initial.subject.result_commit_ref,
|
|
146
|
+
verificationSubject: true });
|
|
147
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace, label: 'verification pre-spawn' });
|
|
148
|
+
observed = await runIsolatedVerificationCheck({ workingDir: workspace.subject_dir,
|
|
149
|
+
executableBinding: check.executable_binding, args: check.args,
|
|
150
|
+
stdin: scripts.get(check.stdin_ref), timeoutMs: check.timeout_ms, subjectReadOnly: true });
|
|
151
|
+
completionProven = verificationProcessCompleted(observed, { workingDir: workspace.subject_dir,
|
|
152
|
+
executableBinding: check.executable_binding, args: check.args, stdin: scripts.get(check.stdin_ref) });
|
|
153
|
+
} catch (error) { errorCode = error.code || 'RAIL_VERIFICATION_CHECK_FAILED'; }
|
|
154
|
+
finally {
|
|
155
|
+
if (workspace && process.platform !== 'darwin') {
|
|
156
|
+
try { cleanupRailReadOnlyCommitWorkspace(workspace); }
|
|
157
|
+
catch (error) { errorCode ??= error.code || 'RAIL_VERIFICATION_CLEANUP_FAILED'; }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const passed = !errorCode && observed?.exit_code === 0 && observed.signal === null
|
|
161
|
+
&& observed.timed_out === false && observed.output_limit_exceeded === false
|
|
162
|
+
&& observed.input_error === null && observed.subject_read_only === true && completionProven;
|
|
163
|
+
// Base64 preserves exact bounded output bytes without JSON control-character
|
|
164
|
+
// expansion or UTF-8 replacement changing the stored content's digest.
|
|
165
|
+
const facts = Object.fromEntries(Object.entries(observed ?? {}).filter(([key]) => !['stdout', 'stderr'].includes(key)));
|
|
166
|
+
const record = { schema_version: 1, evidence_class: 'local-isolated-verification-check', run_id,
|
|
167
|
+
execution_subject: initial.subject, check, observed_at: timestamp(options.clock), passed, error_code: errorCode, facts };
|
|
168
|
+
const persisted = persistReport(options.projectDir, record);
|
|
169
|
+
checks.push({ check_id: check.check_id, kind: check.kind, criterion_refs: check.criterion_refs,
|
|
170
|
+
passed, exit_code: observed?.exit_code ?? null, error_code: errorCode, ...persisted });
|
|
171
|
+
}
|
|
172
|
+
let browserCheck = null;
|
|
173
|
+
if (initial.browser) {
|
|
174
|
+
let workspace; let observed; let errorCode = null; let completionProven = false;
|
|
175
|
+
const frozen = initial.browser.plan.observation;
|
|
176
|
+
const { stdin_ref, ...application } = frozen.application;
|
|
177
|
+
try {
|
|
178
|
+
workspace = createRailReadOnlyCommitWorkspace({ projectDir: options.projectDir,
|
|
179
|
+
baseCommitRef: initial.subject.base_commit_ref, resultCommitRef: initial.subject.result_commit_ref, verificationSubject: true });
|
|
180
|
+
assertRailReadOnlyCommitWorkspaceUnchanged({ workspace, label: 'browser verification pre-spawn' });
|
|
181
|
+
const observation = { workingDir: realpathSync(workspace.subject_dir), ...frozen,
|
|
182
|
+
application: { ...application, stdin: scripts.get(stdin_ref) } };
|
|
183
|
+
observed = await runIsolatedBrowserObservation(observation);
|
|
184
|
+
completionProven = browserObservationCompleted(observed, observation);
|
|
185
|
+
} catch (error) { errorCode = error.code || 'BROWSER_VERIFICATION_FAILED'; }
|
|
186
|
+
finally {
|
|
187
|
+
if (workspace && process.platform !== 'darwin') {
|
|
188
|
+
try { cleanupRailReadOnlyCommitWorkspace(workspace); }
|
|
189
|
+
catch (error) { errorCode ??= error.code || 'BROWSER_VERIFICATION_CLEANUP_FAILED'; }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const captured_at = timestamp(options.clock);
|
|
193
|
+
const passed = !errorCode && observed?.passed === true && observed.tool === initial.browser.plan.tool_id
|
|
194
|
+
&& completionProven;
|
|
195
|
+
const artifacts = [];
|
|
196
|
+
if (passed) {
|
|
197
|
+
for (const artifact of observed.artifacts) artifacts.push(persistBrowserArtifact({ projectDir: options.projectDir,
|
|
198
|
+
projectId: initial.context.handoff.project_id, run_id, attempt_id: options.attempt_id,
|
|
199
|
+
commit_ref: initial.subject.result_commit_ref, policy_ref: initial.browser.policy.policy_id,
|
|
200
|
+
browser_session_id: initial.browser.manifest.browser_session_id, artifact,
|
|
201
|
+
retention_class: initial.browser.policy.artifact_policy.retention_class, captured_at, clock: options.clock }));
|
|
202
|
+
if (artifacts.length === 0) fail('REAL_BROWSER_ARTIFACT_REQUIRED', 'successful observation did not produce a retained browser artifact');
|
|
203
|
+
}
|
|
204
|
+
// Raw/redacted content has one managed retention owner only. Permanent
|
|
205
|
+
// reports and receipts carry descriptors, hashes and process facts, not copies.
|
|
206
|
+
const facts = Object.fromEntries(Object.entries(observed ?? {}).filter(([key]) => key !== 'artifacts'));
|
|
207
|
+
browserCheck = { passed, error_code: errorCode || observed?.error_code || null, observed_at: captured_at,
|
|
208
|
+
artifacts, ...persistReport(options.projectDir, { schema_version: 1, evidence_class: 'local-isolated-browser-verification',
|
|
209
|
+
run_id, execution_subject: initial.subject, plan_digest: sha256(initial.browser.plan), captured_at, passed,
|
|
210
|
+
error_code: errorCode || observed?.error_code || null, artifacts, facts }) };
|
|
211
|
+
}
|
|
212
|
+
const current = canonicalContext(options);
|
|
213
|
+
if (current.binding_digest !== initial.binding_digest) fail('RAIL_VERIFICATION_SUBJECT_CHANGED', 'canonical result, verification plan or certification changed during observation');
|
|
214
|
+
const ok = checks.length === initial.plan.checks.length && checks.every((check) => check.passed) && (!browserCheck || browserCheck.passed);
|
|
215
|
+
const observed_at = timestamp(options.clock);
|
|
216
|
+
let evidence = null;
|
|
217
|
+
if (ok) {
|
|
218
|
+
const receipt = (kind, material, receiptObservedAt = observed_at) => {
|
|
219
|
+
const unsigned = signedRailEvidencePayload({ authority_kind: kind, issuer_id: current.authority.identity.issuer_id,
|
|
220
|
+
project_id: current.context.handoff.project_id, attempt_id: options.attempt_id,
|
|
221
|
+
commit_ref: current.subject.result_commit_ref, verdict: 'passed', evidence_digest: sha256(material), observed_at: receiptObservedAt });
|
|
222
|
+
const proof = signDelegatedAuthorityStatement(unsigned, current.authority.signingOptions);
|
|
223
|
+
const signed = { ...unsigned, signature: proof.signature };
|
|
224
|
+
if (proof.issuer_id !== unsigned.issuer_id || !verifyRailEvidenceReceipt(current.authority.verifier, signed, unsigned, options.clock)) {
|
|
225
|
+
fail('RAIL_VERIFICATION_SIGNATURE_INVALID', 'observed receipt does not validate against the enrolled authority');
|
|
226
|
+
}
|
|
227
|
+
return signed;
|
|
228
|
+
};
|
|
229
|
+
const ciRefs = checks.filter((check) => check.kind === 'ci').map((check) => check.report_ref);
|
|
230
|
+
const acceptanceRefs = checks.filter((check) => check.kind === 'acceptance').map((check) => check.report_ref);
|
|
231
|
+
let browserEvidence;
|
|
232
|
+
if (browserCheck) {
|
|
233
|
+
const delegation = current.browser.manifest;
|
|
234
|
+
assertLocalBrowserArtifacts({ projectDir: options.projectDir, projectId: current.context.handoff.project_id,
|
|
235
|
+
artifacts: browserCheck.artifacts, clock: options.clock, expected: { attempt_id: options.attempt_id,
|
|
236
|
+
commit_ref: current.subject.result_commit_ref, policy_ref: delegation.policy_ref,
|
|
237
|
+
browser_session_id: delegation.browser_session_id, captured_at: browserCheck.observed_at } });
|
|
238
|
+
const material = browserEvidenceMaterial({ schema_version: 1, manifest_type: 'BrowserRuntimeEvidenceManifest',
|
|
239
|
+
evidence_class: 'real-browser', browser_session_id: delegation.browser_session_id, task_id: current.context.task.task_id,
|
|
240
|
+
policy_ref: delegation.policy_ref, tool: delegation.tool, allowed_domain: delegation.allowed_domain,
|
|
241
|
+
classification: delegation.classification, commit_ref: current.subject.result_commit_ref,
|
|
242
|
+
delegation_manifest_hash: delegation.manifest_hash, observed_at: browserCheck.observed_at, artifacts: browserCheck.artifacts,
|
|
243
|
+
...(delegation.tenant_ref ? { tenant_ref: delegation.tenant_ref } : {}) });
|
|
244
|
+
browserEvidence = { ...material, evidence_ref: `browser-evidence://sha256/${sha256(material)}`,
|
|
245
|
+
authority_receipt: receipt('rail_browser', material, browserCheck.observed_at) };
|
|
246
|
+
acceptanceRefs.push(browserCheck.report_ref, ...browserCheck.artifacts.map((artifact) => artifact.sanitized_ref), browserEvidence.evidence_ref);
|
|
247
|
+
}
|
|
248
|
+
let metrics;
|
|
249
|
+
if (current.effort) {
|
|
250
|
+
const metric = (source) => {
|
|
251
|
+
const persisted = persistReport(options.projectDir, { ...source, run_id,
|
|
252
|
+
execution_subject: current.subject, observed_at });
|
|
253
|
+
acceptanceRefs.push(persisted.report_ref);
|
|
254
|
+
return source.evidence_class === 'observed-provider-invocation-wall-time'
|
|
255
|
+
? providerProcessingMetric(source, persisted.report_ref)
|
|
256
|
+
: { unit: 'minutes', value: source.value / 60000, evidence_ref: persisted.report_ref };
|
|
257
|
+
};
|
|
258
|
+
metrics = { actual_human_effort: metric(current.effort.human),
|
|
259
|
+
actual_ai_processing: metric(current.effort.processing),
|
|
260
|
+
source_journal_digest: current.effort.source_journal_digest,
|
|
261
|
+
rework_cycles: current.effort.rework_review_refs.length,
|
|
262
|
+
rework_review_refs: current.effort.rework_review_refs };
|
|
263
|
+
acceptanceRefs.push(completionMetricsReference({ attempt_id: options.attempt_id,
|
|
264
|
+
commit_ref: current.subject.result_commit_ref, metrics }));
|
|
265
|
+
}
|
|
266
|
+
evidence = {
|
|
267
|
+
ci: { verdict: 'passed', commit_ref: current.subject.result_commit_ref, evidence_refs: ciRefs, authority_receipt: receipt('rail_ci', ciRefs) },
|
|
268
|
+
acceptance_evidence_refs: acceptanceRefs, acceptance_authority_receipt: receipt('rail_acceptance', acceptanceRefs),
|
|
269
|
+
...(metrics ? { metrics } : {}),
|
|
270
|
+
...(browserEvidence ? { browser_evidence: browserEvidence } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const report = { schema_version: 1, evidence_class: 'local-isolated-verification', run_id, started_at, observed_at,
|
|
274
|
+
execution_subject: initial.subject, binding_digest: initial.binding_digest, checks, browser: browserCheck, ok, evidence };
|
|
275
|
+
const persisted = persistReport(options.projectDir, report);
|
|
276
|
+
return Object.freeze({ ok, action: 'rail_verification_observed', run_id,
|
|
277
|
+
execution_subject: initial.subject, checks, browser: browserCheck, evidence, ...persisted });
|
|
278
|
+
}
|
|
@@ -3,7 +3,8 @@ import { realpathSync } from 'node:fs';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { acceptReview, decideReviewProgress } from '@chati/review-council';
|
|
5
5
|
import { sha256 } from '@chati/core';
|
|
6
|
-
import {
|
|
6
|
+
import { attemptExecutionBase } from '@chati/rail';
|
|
7
|
+
import { assertRuntimeTaskReviewBindingV2, loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
7
8
|
import {
|
|
8
9
|
createProjectRailEngine, loadLatestRailExecutionEvidence, loadRailHandoff, resolveRailAttemptContext,
|
|
9
10
|
} from './rail-runtime.js';
|
|
@@ -13,6 +14,36 @@ import { assertRailReadOnlyCommitWorkspaceUnchanged } from '../terminal/rail-rea
|
|
|
13
14
|
|
|
14
15
|
function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
|
|
15
16
|
|
|
17
|
+
function assertSealedTaskReviewInvocation({ artifact, task, invocation, clock }) {
|
|
18
|
+
if (!task?.review_binding) return;
|
|
19
|
+
const expected = assertRuntimeTaskReviewBindingV2({
|
|
20
|
+
artifact,
|
|
21
|
+
review_binding: task.review_binding,
|
|
22
|
+
builder_identity: {
|
|
23
|
+
provider_id: task.execution_binding.provider_id,
|
|
24
|
+
model_id: task.execution_binding.model_id,
|
|
25
|
+
},
|
|
26
|
+
...(clock === undefined ? {} : { clock }),
|
|
27
|
+
});
|
|
28
|
+
const actual = {
|
|
29
|
+
provider_id: invocation?.provider_id,
|
|
30
|
+
harness_id: invocation?.harness_id,
|
|
31
|
+
model_id: invocation?.model_pin?.model_id,
|
|
32
|
+
reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
|
|
33
|
+
catalog_snapshot_ref: invocation?.model_pin?.catalog_snapshot_ref,
|
|
34
|
+
};
|
|
35
|
+
const required = {
|
|
36
|
+
provider_id: expected.provider_id,
|
|
37
|
+
harness_id: expected.harness_id,
|
|
38
|
+
model_id: expected.model_pin.model_id,
|
|
39
|
+
reasoning_configuration: expected.model_pin.reasoning_configuration,
|
|
40
|
+
catalog_snapshot_ref: expected.model_pin.catalog_snapshot_ref,
|
|
41
|
+
};
|
|
42
|
+
if (Object.entries(required).some(([field, value]) => actual[field] !== value)) {
|
|
43
|
+
fail('RAIL_REVIEW_ROUTE_MISMATCH', 'review invocation does not match the sealed task review binding');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
function trustedGitEnvironment(source = process.env) {
|
|
17
48
|
const environment = Object.fromEntries(Object.entries(source).filter(([key]) => !key.startsWith('GIT_')));
|
|
18
49
|
return {
|
|
@@ -80,6 +111,7 @@ export function recordRailReviewDispatch({
|
|
|
80
111
|
})) fail('UNTRUSTED_REVIEW_LAUNCH', 'review dispatch requires a terminal-authority launch receipt');
|
|
81
112
|
const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
|
|
82
113
|
if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review dispatch requires a v2 installation');
|
|
114
|
+
assertSealedTaskReviewInvocation({ artifact, task: context.task, invocation, clock });
|
|
83
115
|
return engine.recordReviewDispatch({
|
|
84
116
|
attempt_id, invocation, launch_receipt,
|
|
85
117
|
installation: artifact.installation, snapshot: artifact.capability_snapshot,
|
|
@@ -104,7 +136,7 @@ function resolveAttemptLineage(projectDir, attemptId, clock) {
|
|
|
104
136
|
handoff_id: handoff.handoff_id,
|
|
105
137
|
handoff_manifest_digest: handoff.seal.manifest_digest,
|
|
106
138
|
task_id: attempt.task_id,
|
|
107
|
-
base_commit_ref: attempt
|
|
139
|
+
base_commit_ref: attemptExecutionBase(attempt),
|
|
108
140
|
});
|
|
109
141
|
}
|
|
110
142
|
|
|
@@ -181,6 +213,9 @@ export function recordRailReview({
|
|
|
181
213
|
}
|
|
182
214
|
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
183
215
|
if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review requires a v2 installation');
|
|
216
|
+
const reviewLineage = resolveAttemptLineage(projectDir, review?.attempt_id, clock);
|
|
217
|
+
const reviewContext = resolveRailAttemptContext({ projectDir, handoff_id: reviewLineage.handoff_id, attempt_id: review?.attempt_id, clock });
|
|
218
|
+
assertSealedTaskReviewInvocation({ artifact, task: reviewContext.task, invocation, clock });
|
|
184
219
|
const validated = acceptReview({ review, invocation, installation: artifact.installation, snapshot: artifact.capability_snapshot, clock });
|
|
185
220
|
const baseLineage = resolveAttemptLineage(projectDir, validated.attempt_id, clock);
|
|
186
221
|
const engine = createProjectRailEngine({ projectDir, clock });
|
|
@@ -2,7 +2,7 @@ import { doctorV2 } from '../installer-v2/index.js';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { assertEligibleInvocation } from '@chati/provider-registry';
|
|
5
|
-
import { selectCanonicalReviewer } from '@chati/review-council';
|
|
5
|
+
import { selectCanonicalReviewer, selectCouncilReviewers } from '@chati/review-council';
|
|
6
6
|
import {
|
|
7
7
|
readInstallationArtifactFile, verifyInstallationCatalogProvenance,
|
|
8
8
|
} from '../installer-v2/catalog-client.js';
|
|
@@ -19,33 +19,36 @@ export const V2_PLANNING_AGENTS = Object.freeze([
|
|
|
19
19
|
'detail', 'architect', 'ux', 'qa-planning', 'phases', 'tasks',
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
brief: Object.freeze(['work-understanding']),
|
|
24
|
-
detail: Object.freeze(['work-understanding', 'brief']),
|
|
25
|
-
architect: Object.freeze(['work-understanding', 'brief']),
|
|
26
|
-
ux: Object.freeze(['work-understanding', 'brief']),
|
|
27
|
-
'qa-planning': Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux']),
|
|
28
|
-
phases: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning']),
|
|
29
|
-
tasks: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning', 'phases']),
|
|
30
|
-
});
|
|
22
|
+
const COUNCIL_GATE_STAGES = new Set(['discovery', 'planning', 'execution-design']);
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const error = new Error(`Agent ${agent} cannot advance before its canonical predecessors complete: ${incomplete.join(', ')}`);
|
|
42
|
-
error.code = 'V2_PLANNING_PREDECESSORS_INCOMPLETE';
|
|
43
|
-
error.incomplete = incomplete;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the installation-owned council contract for one stage. Artifacts
|
|
26
|
+
* created before this policy existed deliberately remain non-disruptive until
|
|
27
|
+
* their owner explicitly reconfigures the installation.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveRuntimeCouncilGatePolicyV2({ artifact, stage } = {}) {
|
|
30
|
+
if (!COUNCIL_GATE_STAGES.has(stage)) {
|
|
31
|
+
const error = new Error(`Unknown council stage: ${stage}`);
|
|
32
|
+
error.code = 'COUNCIL_GATE_STAGE_INVALID';
|
|
44
33
|
throw error;
|
|
45
34
|
}
|
|
46
|
-
|
|
35
|
+
const policy = artifact?.installation?.quality_council;
|
|
36
|
+
if (!policy || policy.mode !== 'autonomous' || !policy.stages?.includes(stage)) {
|
|
37
|
+
return Object.freeze({ enabled: false, reason: policy?.mode === 'guided' ? 'GUIDED_COUNCIL_REQUIRED' : 'COUNCIL_POLICY_NOT_CONFIGURED' });
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
enabled: true,
|
|
41
|
+
policy: Object.freeze({
|
|
42
|
+
min_members: policy.min_members,
|
|
43
|
+
max_members: policy.max_members,
|
|
44
|
+
require_distinct_providers: policy.require_distinct_providers,
|
|
45
|
+
blocking_severities: [...policy.blocking_severities],
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export { assertV2PlanningPredecessors } from './agent-selector.js';
|
|
51
|
+
|
|
49
52
|
const RAIL_ACTIONS = new Set(['build', 'review']);
|
|
50
53
|
const REASONING_CONFIGURATIONS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
51
54
|
|
|
@@ -169,6 +172,15 @@ export function resolveRuntimeExecutableBindingV2({ artifact, provider_id, harne
|
|
|
169
172
|
});
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
function routingPriority(model, routingKey, action) {
|
|
176
|
+
const keys = Array.isArray(routingKey) ? routingKey : [routingKey];
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
const priority = model.routing_priority?.[key];
|
|
179
|
+
if (priority !== undefined) return priority;
|
|
180
|
+
}
|
|
181
|
+
return model.routing_priority?.[action] ?? 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
function resolveRuntimeActionV2({ artifact, action, routingKey, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
|
|
173
185
|
let hasEligibleModel = false;
|
|
174
186
|
const candidates = artifact.installation.enabled_providers.flatMap((candidateBinding) => {
|
|
@@ -193,7 +205,7 @@ function resolveRuntimeActionV2({ artifact, action, routingKey, binding, model_i
|
|
|
193
205
|
binding: candidateBinding,
|
|
194
206
|
model: catalogModel,
|
|
195
207
|
reasoning,
|
|
196
|
-
priority: catalogModel
|
|
208
|
+
priority: routingPriority(catalogModel, routingKey, action),
|
|
197
209
|
}];
|
|
198
210
|
});
|
|
199
211
|
}).sort((left, right) =>
|
|
@@ -241,6 +253,93 @@ export function resolveRuntimeRailInvocationV2({ artifact, action = 'build', bin
|
|
|
241
253
|
return resolveRuntimeActionV2({ artifact, action, routingKey: `rail:${action}`, binding, model_id, reasoning_configuration, clock });
|
|
242
254
|
}
|
|
243
255
|
|
|
256
|
+
const TASK_RISK_LEVELS = new Set(['low', 'medium', 'high', 'critical']);
|
|
257
|
+
|
|
258
|
+
function normalizeTaskRoutingProfile(profile = {}) {
|
|
259
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile) || !TASK_RISK_LEVELS.has(profile.risk_level)) {
|
|
260
|
+
const error = new Error('task routing_profile.risk_level must be low, medium, high or critical');
|
|
261
|
+
error.code = 'TASK_ROUTING_PROFILE_REQUIRED';
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
return Object.freeze({ risk_level: profile.risk_level });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Selects the exact build and independent-review bindings at planning seal
|
|
269
|
+
* time. The model catalog owns the priorities, so an updated signed catalog
|
|
270
|
+
* can change the route without embedding personal ETCAR weights in CHATI.
|
|
271
|
+
*/
|
|
272
|
+
export function resolveRuntimeTaskRouteV2({ artifact, routing_profile, required_independence_level = 'A', clock = () => new Date() } = {}) {
|
|
273
|
+
if (!['A', 'B', 'C'].includes(required_independence_level)) {
|
|
274
|
+
const error = new Error('required_independence_level must be A, B or C');
|
|
275
|
+
error.code = 'TASK_ROUTING_PROFILE_INVALID';
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
const profile = normalizeTaskRoutingProfile(routing_profile);
|
|
279
|
+
const execution = resolveRuntimeActionV2({
|
|
280
|
+
artifact,
|
|
281
|
+
action: 'build',
|
|
282
|
+
routingKey: [`task:build:${profile.risk_level}`, 'task:build', 'rail:build'],
|
|
283
|
+
clock,
|
|
284
|
+
});
|
|
285
|
+
const builder_identity = { provider_id: execution.provider_id, model_id: execution.model_pin.model_id };
|
|
286
|
+
const selectedReview = resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock });
|
|
287
|
+
const attained = selectedReview.independence_level;
|
|
288
|
+
const strength = { A: 3, B: 2, C: 1 };
|
|
289
|
+
if (strength[attained] < strength[required_independence_level]) {
|
|
290
|
+
const error = new Error(`task requires independence ${required_independence_level}, but the selected catalog route attains ${attained}`);
|
|
291
|
+
error.code = 'TASK_REVIEW_INDEPENDENCE_UNAVAILABLE';
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
routing_profile: profile,
|
|
296
|
+
execution_binding: Object.freeze({
|
|
297
|
+
provider_id: execution.provider_id,
|
|
298
|
+
harness_id: execution.harness_id,
|
|
299
|
+
model_id: execution.model_pin.model_id,
|
|
300
|
+
reasoning_configuration: execution.model_pin.reasoning_configuration,
|
|
301
|
+
}),
|
|
302
|
+
review_binding: Object.freeze({
|
|
303
|
+
provider_id: selectedReview.provider_id,
|
|
304
|
+
harness_id: selectedReview.harness_id,
|
|
305
|
+
model_id: selectedReview.model_pin.model_id,
|
|
306
|
+
reasoning_configuration: selectedReview.model_pin.reasoning_configuration,
|
|
307
|
+
attained_independence_level: attained,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Validates a sealed task review binding without allowing a runtime reroute. */
|
|
313
|
+
export function assertRuntimeTaskReviewBindingV2({ artifact, review_binding, builder_identity, clock = () => new Date() } = {}) {
|
|
314
|
+
if (!review_binding || typeof review_binding !== 'object' || Array.isArray(review_binding)) {
|
|
315
|
+
const error = new Error('sealed task review binding is required');
|
|
316
|
+
error.code = 'TASK_REVIEW_BINDING_REQUIRED';
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
const invocation = {
|
|
320
|
+
provider_id: review_binding.provider_id,
|
|
321
|
+
harness_id: review_binding.harness_id,
|
|
322
|
+
action: 'review',
|
|
323
|
+
model_pin: {
|
|
324
|
+
model_id: review_binding.model_id,
|
|
325
|
+
catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
|
|
326
|
+
reasoning_configuration: review_binding.reasoning_configuration,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
|
|
330
|
+
const reviewer_identity = { provider_id: invocation.provider_id, model_id: invocation.model_pin.model_id };
|
|
331
|
+
const independence_level = builder_identity
|
|
332
|
+
? (builder_identity.provider_id !== reviewer_identity.provider_id ? 'A'
|
|
333
|
+
: builder_identity.model_id !== reviewer_identity.model_id ? 'B' : 'C')
|
|
334
|
+
: null;
|
|
335
|
+
if (review_binding.attained_independence_level !== undefined && review_binding.attained_independence_level !== independence_level) {
|
|
336
|
+
const error = new Error('sealed task review binding independence level drifted');
|
|
337
|
+
error.code = 'TASK_REVIEW_BINDING_DRIFT';
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
return Object.freeze({ ...invocation, reviewer_identity, independence_level });
|
|
341
|
+
}
|
|
342
|
+
|
|
244
343
|
/** Selects the highest-ranked installed reviewer at the strongest available independence level. */
|
|
245
344
|
export function resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock = () => new Date() }) {
|
|
246
345
|
return selectCanonicalReviewer({
|
|
@@ -251,6 +350,26 @@ export function resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock
|
|
|
251
350
|
});
|
|
252
351
|
}
|
|
253
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Resolves the planning-quality council from the providers the user selected
|
|
355
|
+
* during installation. The council is two or three distinct providers, never
|
|
356
|
+
* a hidden provider fallback and never a hard-coded vendor trio.
|
|
357
|
+
*/
|
|
358
|
+
export function resolveRuntimeCouncilInvocationsV2({
|
|
359
|
+
artifact,
|
|
360
|
+
builder_identity,
|
|
361
|
+
policy,
|
|
362
|
+
clock = () => new Date(),
|
|
363
|
+
} = {}) {
|
|
364
|
+
return selectCouncilReviewers({
|
|
365
|
+
installation: artifact.installation,
|
|
366
|
+
snapshot: artifact.capability_snapshot,
|
|
367
|
+
builder_identity,
|
|
368
|
+
...(policy ? { policy } : {}),
|
|
369
|
+
clock,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
254
373
|
export function resolveRuntimeAdjudicationV2({ artifact, clock = () => new Date() }) {
|
|
255
374
|
const candidates = artifact.installation.enabled_providers.flatMap((binding) =>
|
|
256
375
|
binding.allowed_models.flatMap((modelId) => {
|