great-cto 3.76.0 → 3.77.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/board/.claude-plugin/plugin.json +3 -2
- package/board/agents/devops.md +19 -0
- package/board/agents/e2e-test-engineer.md +20 -1
- package/board/agents/release-manager.md +213 -0
- package/board/agents/security-officer.md +53 -14
- package/board/packages/board/lib/installed-version.mjs +52 -0
- package/board/packages/board/lib/routes.mjs +5 -12
- package/board/packages/board/public/assets/agent-groups.js +1 -1
- package/board/packages/board/public/assets/vendor/PROVENANCE.md +5 -3
- package/board/packages/board/public/assets/vendor/purify.min.js +7 -3
- package/board/packages/board/public/index.html +3 -3
- package/board/scripts/codex-pipeline.mjs +57 -12
- package/board/scripts/lib/approval-level.mjs +9 -2
- package/board/scripts/lib/codex-pipeline.mjs +114 -29
- package/board/scripts/lib/codex-role-profiles.mjs +1 -0
- package/board/scripts/lib/exceptions.mjs +167 -0
- package/board/scripts/lib/gate-check-bound.mjs +478 -0
- package/board/scripts/lib/gate-check.mjs +173 -0
- package/board/scripts/lib/gate-reversibility.mjs +4 -0
- package/board/scripts/lib/gate-tier.mjs +1 -1
- package/board/scripts/lib/ship-evidence-context.mjs +174 -0
- package/board/scripts/lib/ship-evidence-identity.mjs +294 -0
- package/board/scripts/lib/work-tasks.mjs +16 -2
- package/board/shared/pipeline.toml +41 -0
- package/package.json +1 -1
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, lstatSync, realpathSync, existsSync, mkdirSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, relative, isAbsolute, sep, dirname } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { stableDigest, evaluateBoundShipEvidence, normalizeLegacyOrigins } from './ship-evidence-identity.mjs';
|
|
6
|
+
import { readShipPolicy, captureShipContext, recheckShipContext, assertCommittedShipArtifacts } from './ship-evidence-context.mjs';
|
|
7
|
+
import { codexRunStore } from './codex-host-state.mjs';
|
|
8
|
+
import { normalizeTask } from './gate-check.mjs';
|
|
9
|
+
import { parseVerdictLine } from './verdict-record.mjs';
|
|
10
|
+
import { canonicalAgent, verdictKind } from './ship-evidence.mjs';
|
|
11
|
+
import { invocationIdentity } from './invocation-identity.mjs';
|
|
12
|
+
import { isMainModule } from './is-main.mjs';
|
|
13
|
+
|
|
14
|
+
const brands = new WeakMap();
|
|
15
|
+
const caps = new WeakMap();
|
|
16
|
+
const sha = x => createHash('sha256').update(x).digest('hex');
|
|
17
|
+
const fail = (code, extra = {}) => { throw Object.assign(new Error(code), {code, ...extra}); };
|
|
18
|
+
const uuid = x => typeof x === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(x);
|
|
19
|
+
const hex = x => typeof x === 'string' && /^[a-f0-9]{64}$/.test(x);
|
|
20
|
+
const positive = x => Number.isSafeInteger(x) && x > 0;
|
|
21
|
+
const id = x => typeof x === 'string' && Buffer.byteLength(x) > 0 && Buffer.byteLength(x) <= 256 && !/[\x00-\x1f\x7f-\x9f]/u.test(x);
|
|
22
|
+
const exact = (x, keys) => x && Object.getPrototypeOf(x) === Object.prototype && Object.keys(x).sort().join('|') === [...keys].sort().join('|');
|
|
23
|
+
const clone = x => structuredClone(x);
|
|
24
|
+
const inside = (a, b) => { const r = relative(a, b); return r === '' || (!isAbsolute(r) && r !== '..' && !r.startsWith(`..${sep}`)); };
|
|
25
|
+
const codepointOrder = (a, b) => {
|
|
26
|
+
const aa = Array.from(a, c => c.codePointAt(0)), bb = Array.from(b, c => c.codePointAt(0));
|
|
27
|
+
for (let i = 0; i < Math.min(aa.length, bb.length); i++) if (aa[i] !== bb[i]) return aa[i] - bb[i];
|
|
28
|
+
return aa.length - bb.length;
|
|
29
|
+
};
|
|
30
|
+
// JSON.stringify on a reconstructed object would reorder integer-looking keys.
|
|
31
|
+
// Serialize the ordered key/value pairs directly, matching stableDigest exactly.
|
|
32
|
+
const bytesOf = x => Array.isArray(x) ? `[${x.map(bytesOf).join(',')}]`
|
|
33
|
+
: x !== null && typeof x === 'object' ? `{${Object.keys(x).sort(codepointOrder).map(k => `${JSON.stringify(k)}:${bytesOf(x[k])}`).join(',')}}`
|
|
34
|
+
: JSON.stringify(x);
|
|
35
|
+
function privatePath(path, kind, limit = 16777216) {
|
|
36
|
+
if (!isAbsolute(path) || resolve(path) !== path || realpathSync(path) !== path) fail('SHIP_PATH_UNSAFE');
|
|
37
|
+
const st = lstatSync(path);
|
|
38
|
+
if (st.isSymbolicLink() || (kind === 'dir' ? !st.isDirectory() : !st.isFile()) || st.uid !== process.getuid() || (st.mode & 0o777) !== (kind === 'dir' ? 0o700 : 0o600) || (kind === 'file' && st.size > limit)) fail('SHIP_PATH_UNSAFE');
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
function safeStores(root, store, runStore) {
|
|
42
|
+
root = realpathSync(root); privatePath(store, 'dir'); privatePath(runStore, 'dir');
|
|
43
|
+
if (inside(root, store) || inside(root, runStore) || inside(store, runStore) || inside(runStore, store)) fail('SHIP_PATH_UNSAFE');
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
function readJSON(path) {
|
|
47
|
+
privatePath(path, 'file');
|
|
48
|
+
const value = JSON.parse(readFileSync(path, 'utf8')); stableDigest(value); return value;
|
|
49
|
+
}
|
|
50
|
+
function ensureDirectory(path) {
|
|
51
|
+
if (!existsSync(path)) mkdirSync(path, {mode: 0o700});
|
|
52
|
+
privatePath(path, 'dir'); return path;
|
|
53
|
+
}
|
|
54
|
+
function runDirectory(store, runId) {
|
|
55
|
+
if (!uuid(runId)) fail('SHIP_UNBOUND');
|
|
56
|
+
return join(store, 'runs', runId);
|
|
57
|
+
}
|
|
58
|
+
function evidenceDirectories(store, runId) {
|
|
59
|
+
ensureDirectory(join(store, 'runs'));
|
|
60
|
+
const dir = ensureDirectory(runDirectory(store, runId));
|
|
61
|
+
ensureDirectory(join(dir, 'generations')); ensureDirectory(join(dir, 'blobs')); return dir;
|
|
62
|
+
}
|
|
63
|
+
function readBinding(state) {
|
|
64
|
+
const b = state?.shipEvidence;
|
|
65
|
+
if (!b) fail('SHIP_UNBOUND');
|
|
66
|
+
if (!exact(b, ['version', 'revision', 'phase', 'policyPath', 'policyDigest', 'registrationDigest', 'storeDigest', 'contextDigest', 'assignmentStoreDigest', 'originRegistryDigest', 'generationDigest']) || b.version !== 'ship-bound-run/v1' || !positive(b.revision) || !['PROVISIONAL', 'NEEDS_REASSESSMENT', 'REVIEWING', 'READY', 'INVALIDATED'].includes(b.phase) || typeof b.policyPath !== 'string' || ![b.policyDigest, b.registrationDigest, b.storeDigest, b.assignmentStoreDigest, b.originRegistryDigest, b.generationDigest].every(hex) || !(b.contextDigest === null || hex(b.contextDigest))) fail('SHIP_INVALID_REVISION');
|
|
67
|
+
return b;
|
|
68
|
+
}
|
|
69
|
+
function linked(state, descriptor, store, {pointer = true} = {}) {
|
|
70
|
+
const b = readBinding(state);
|
|
71
|
+
if (!uuid(state.id) || !uuid(state.taskId) || state.version !== 1 || realpathSync(state.root) !== descriptor.root || b.policyPath !== descriptor.policyPath || b.policyDigest !== descriptor.policyDigest || b.registrationDigest !== descriptor.registrationDigest || b.storeDigest !== sha(store)) fail('SHIP_UNBOUND');
|
|
72
|
+
const dir = runDirectory(store, state.id); privatePath(join(store, 'runs'), 'dir'); privatePath(dir, 'dir'); privatePath(join(dir, 'generations'), 'dir');
|
|
73
|
+
privatePath(join(dir, 'blobs'), 'dir');
|
|
74
|
+
const path = join(dir, 'generations', `${b.revision}.json`), gen = readJSON(path);
|
|
75
|
+
if (!exact(gen, ['version', 'runId', 'taskId', 'revision', 'phase', 'policyDigest', 'registrationDigest', 'expected', 'assignments', 'records', 'originRegistry', 'obligations', 'resolutions', 'readStatus', 'taskSnapshotDigest', 'journalSnapshotDigest', 'completionDigests', 'previousGenerationDigest']) || gen.version !== 'ship-generation/v1' || gen.runId !== state.id || gen.taskId !== state.taskId || gen.revision !== b.revision || gen.phase !== b.phase || gen.policyDigest !== b.policyDigest || gen.registrationDigest !== b.registrationDigest || stableDigest(gen) !== b.generationDigest || sha(readFileSync(path)) !== b.generationDigest || stableDigest(gen.assignments) !== b.assignmentStoreDigest || stableDigest(gen.originRegistry) !== b.originRegistryDigest || (gen.expected === null ? b.contextDigest !== null : stableDigest(gen.expected) !== b.contextDigest)) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
76
|
+
for (const name of ['assignments', 'records', 'obligations', 'resolutions', 'completionDigests']) if (!Array.isArray(gen[name]) || gen[name].length > 10000) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
77
|
+
if (gen.resolutions.some(r => r.kind === 'OWNER_SUPERSESSION')) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
78
|
+
for (const field of ['taskSnapshotDigest', 'journalSnapshotDigest']) {
|
|
79
|
+
if (gen[field] === null) continue;
|
|
80
|
+
if (!hex(gen[field]) || stableDigest(readJSON(join(dir, 'blobs', `${gen[field]}.json`))) !== gen[field]) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
81
|
+
}
|
|
82
|
+
for (const digest of gen.completionDigests) {
|
|
83
|
+
if (!hex(digest)) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
84
|
+
const blob = readJSON(join(dir, 'blobs', `${digest}.json`));
|
|
85
|
+
if (stableDigest(blob) !== digest || blob.version !== 'ship-completion/v1' || !gen.records.some(r => r.recordId === blob.recordId && r.reviewerInvocationId === blob.reviewerInvocationId && r.rawRecordDigest === digest) || !gen.assignments.some(a => a.reviewerInvocationId === blob.reviewerInvocationId && (stableDigest(a) === blob.assignmentDigest || a.revoked && stableDigest({...a, revoked: false}) === blob.assignmentDigest))) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
86
|
+
}
|
|
87
|
+
if (gen.records.some(r => !gen.completionDigests.includes(r.rawRecordDigest))) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
88
|
+
if (pointer) {
|
|
89
|
+
const p = readJSON(join(dir, 'current.json'));
|
|
90
|
+
if (!exact(p, ['version', 'generation', 'digest']) || p.version !== 'ship-store-pointer/v1' || p.generation !== b.revision || p.digest !== b.generationDigest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
91
|
+
}
|
|
92
|
+
return gen;
|
|
93
|
+
}
|
|
94
|
+
function now(clock) { const n = clock(); if (!Number.isSafeInteger(n) || n < 0) fail('SHIP_EVIDENCE_UNKNOWN'); return n; }
|
|
95
|
+
const unknownRead = subject => ({state: 'UNKNOWN', snapshotDigest: null, diagnostics: [{code: 'READ_UNKNOWN', subject}]});
|
|
96
|
+
function productionTasks({root}) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = execFileSync('bd', ['--readonly', '--sandbox', 'list', '--all', '--limit', '0', '--include-gates', '--include-infra', '--include-templates', '--include-ephemeral', '--max-rows', '10000', '--json'], {cwd: root, encoding: 'utf8', timeout: 20000, maxBuffer: 16777216, stdio: ['ignore', 'pipe', 'ignore'], env: {...process.env, BD_DISABLE_METRICS: '1', BD_DISABLE_EVENT_FLUSH: '1', BD_DOLT_AUTO_PUSH: 'false'}});
|
|
99
|
+
const parsed = JSON.parse(raw), tasks = Array.isArray(parsed) ? parsed : parsed.issues || parsed.tasks;
|
|
100
|
+
if (!Array.isArray(tasks) || tasks.length > 10000) throw Error();
|
|
101
|
+
return {state: 'COMPLETE', tasks, snapshotDigest: stableDigest(tasks), diagnostics: []};
|
|
102
|
+
} catch { return {...unknownRead('tasks'), tasks: []}; }
|
|
103
|
+
}
|
|
104
|
+
function productionJournals({journalRoot, originId}) {
|
|
105
|
+
const journals = []; let total = 0, complete = true;
|
|
106
|
+
try {
|
|
107
|
+
privatePath(journalRoot, 'dir');
|
|
108
|
+
const files = readdirSync(journalRoot).sort();
|
|
109
|
+
if (files.length > 10000) throw Error();
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
if (!file.endsWith('.log')) continue;
|
|
112
|
+
if (!id(file)) throw Error();
|
|
113
|
+
const path = privatePath(join(journalRoot, file), 'file'), raw = readFileSync(path);
|
|
114
|
+
if ((total += raw.length) > 16777216) throw Error();
|
|
115
|
+
const records = [];
|
|
116
|
+
for (const line of raw.toString('utf8').split('\n')) {
|
|
117
|
+
if (!line.trim()) continue;
|
|
118
|
+
if (records.length >= 10000 || Buffer.byteLength(line) > 262144) throw Error();
|
|
119
|
+
const parsed = parseVerdictLine(line), rawDigest = sha(line);
|
|
120
|
+
let outcome = 'UNKNOWN', role = canonicalAgent(file);
|
|
121
|
+
if (parsed.ok && Number.isFinite(Date.parse(parsed.rec.ts))) {
|
|
122
|
+
role = canonicalAgent(parsed.rec.agent || file);
|
|
123
|
+
const kind = verdictKind(parsed.rec.verdict);
|
|
124
|
+
outcome = kind === 'negative' ? 'FAIL' : kind === 'positive' ? 'PASS' : 'UNKNOWN';
|
|
125
|
+
if (kind === 'neutral') complete = false;
|
|
126
|
+
} else complete = false;
|
|
127
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(role)) { role = 'unknown-reviewer'; complete = false; }
|
|
128
|
+
records.push({rawDigest, outcome, role, rawRecord: parsed.ok ? parsed.rec : {rejectedDigest: rawDigest}});
|
|
129
|
+
}
|
|
130
|
+
journals.push({journalId: file, originId, snapshotId: sha(raw), complete: true, records});
|
|
131
|
+
}
|
|
132
|
+
return {state: complete ? 'COMPLETE' : 'UNKNOWN', journals, snapshotDigest: stableDigest(journals), diagnostics: complete ? [] : [{code: 'JOURNAL_REJECTED', subject: 'journals'}]};
|
|
133
|
+
} catch { return {...unknownRead('journals'), journals}; }
|
|
134
|
+
}
|
|
135
|
+
function snapshots(root, descriptor, registry, clock, readTasks, readJournals) {
|
|
136
|
+
let t, j, invalid = false;
|
|
137
|
+
try { t = (readTasks || productionTasks)({root}); stableDigest(t); } catch { t = {...unknownRead('tasks'), tasks: []}; }
|
|
138
|
+
try { j = (readJournals || productionJournals)({root, journalRoot: descriptor.policy.journalRoot, originId: descriptor.registration.journalOriginId}); stableDigest(j); } catch { j = {...unknownRead('journals'), journals: []}; }
|
|
139
|
+
const tasks = [];
|
|
140
|
+
if (!Array.isArray(t.tasks) || t.tasks.length > 10000) invalid = true;
|
|
141
|
+
else for (const raw of t.tasks) {
|
|
142
|
+
if (!raw || !id(raw.id) || typeof raw.status !== 'string' || !Array.isArray(raw.labels) || raw.labels.some(l => typeof l !== 'string')) { invalid = true; continue; }
|
|
143
|
+
const {id: taskId, state} = normalizeTask(raw);
|
|
144
|
+
if (!['open', 'in_progress', 'closed', 'blocked', 'failed', 'unverified', 'not_run', 'not-run'].includes(state)) { invalid = true; continue; }
|
|
145
|
+
tasks.push({id: taskId, state});
|
|
146
|
+
}
|
|
147
|
+
let journals = [];
|
|
148
|
+
try {
|
|
149
|
+
if (!Array.isArray(j.journals) || j.journals.length > 10000) throw Error();
|
|
150
|
+
journals = j.journals.map(v => {
|
|
151
|
+
if (v.originId !== descriptor.registration.journalOriginId || !Array.isArray(v.records)) throw Error();
|
|
152
|
+
return {journalId: v.journalId, originId: v.originId, snapshotId: v.snapshotId, complete: v.complete, records: v.records.map(r => ({rawDigest: r.rawDigest, outcome: r.outcome, role: r.role}))};
|
|
153
|
+
});
|
|
154
|
+
} catch { invalid = true; }
|
|
155
|
+
const normalized = normalizeLegacyOrigins({registry, journals});
|
|
156
|
+
const status = {version: 'ship-read-status/v1', nowMs: now(clock), context: 'COMPLETE', assignments: 'COMPLETE', journals: j.state === 'COMPLETE' && !invalid ? 'COMPLETE' : 'UNKNOWN', tasks: t.state === 'COMPLETE' && !invalid ? 'COMPLETE' : 'UNKNOWN', policy: 'COMPLETE', origins: normalized.state === 'UNKNOWN' ? 'UNKNOWN' : 'COMPLETE', trustedOwnerDecisions: []};
|
|
157
|
+
const taskSnapshotDigest = Array.isArray(t.tasks) && stableDigest(t.tasks) === t.snapshotDigest ? t.snapshotDigest : null;
|
|
158
|
+
const journalSnapshotDigest = Array.isArray(j.journals) && stableDigest(j.journals) === j.snapshotDigest ? j.snapshotDigest : null;
|
|
159
|
+
if (!hex(taskSnapshotDigest)) status.tasks = 'UNKNOWN';
|
|
160
|
+
if (!hex(journalSnapshotDigest)) status.journals = 'UNKNOWN';
|
|
161
|
+
return {tasks, normalized, status, taskSnapshotDigest, journalSnapshotDigest, rawTasks: t, rawJournals: j};
|
|
162
|
+
}
|
|
163
|
+
function requirementsFor(descriptor, state) {
|
|
164
|
+
const actors = [...new Set((state.attempts || []).filter(a => a.role === 'senior-dev' && id(a.invocationId)).map(a => a.invocationId))].sort();
|
|
165
|
+
return descriptor.requirements.map(r => ({...r, independentOf: actors}));
|
|
166
|
+
}
|
|
167
|
+
function innerEvaluation(state, descriptor, gen, options, {allowAppend = false} = {}) {
|
|
168
|
+
const snaps = snapshots(descriptor.root, descriptor, gen.originRegistry, options.clock, options.readTasks, options.readJournals);
|
|
169
|
+
if (!gen.expected || !['REVIEWING', 'READY'].includes(gen.phase)) snaps.status.context = 'UNKNOWN';
|
|
170
|
+
else {
|
|
171
|
+
if (recheckShipContext({root: descriptor.root, registration: descriptor.registration, expected: gen.expected}).state !== 'MATCH') snaps.status.context = 'UNKNOWN';
|
|
172
|
+
if (stableDigest(requirementsFor(descriptor, state)) !== gen.expected.requirementsDigest) snaps.status.policy = 'UNKNOWN';
|
|
173
|
+
}
|
|
174
|
+
if (!allowAppend && stableDigest(snaps.normalized.registry) !== stableDigest(gen.originRegistry)) snaps.status.origins = 'UNKNOWN';
|
|
175
|
+
const result = evaluateBoundShipEvidence({expected: gen.expected, invocations: gen.assignments, records: gen.records, obligations: [...gen.obligations, ...snaps.normalized.obligations], resolutions: gen.resolutions, tasks: snaps.tasks, readStatus: snaps.status});
|
|
176
|
+
return {result, snaps};
|
|
177
|
+
}
|
|
178
|
+
const unknownReport = () => ({version: 'ship-bound-gate/v1', state: 'UNKNOWN', contextDigest: null, revision: null, result: evaluateBoundShipEvidence(null), publicationEligibility: 'NOT_CHECKED'});
|
|
179
|
+
function knownHistoricalFindings({root, store, runId, expectedRevision}, {runStore, clock}) {
|
|
180
|
+
const report = unknownReport();
|
|
181
|
+
try {
|
|
182
|
+
root = safeStores(root, store, runStore);
|
|
183
|
+
if (!uuid(runId) || !positive(expectedRevision)) return report;
|
|
184
|
+
const state = readJSON(join(runStore, `${runId}.json`)), binding = readBinding(state);
|
|
185
|
+
if (state.id !== runId || state.version !== 1 || !uuid(state.taskId) || realpathSync(state.root) !== root || binding.revision !== expectedRevision || binding.storeDigest !== sha(store) || binding.policyPath !== join(store, 'policy.json')) return report;
|
|
186
|
+
const dir = runDirectory(store, runId);
|
|
187
|
+
for (const path of [join(store, 'runs'), dir, join(dir, 'generations')]) privatePath(path, 'dir');
|
|
188
|
+
const path = join(dir, 'generations', `${binding.revision}.json`), generation = readJSON(path);
|
|
189
|
+
if (sha(readFileSync(path)) !== binding.generationDigest || stableDigest(generation) !== binding.generationDigest || generation.version !== 'ship-generation/v1' || generation.runId !== runId || generation.taskId !== state.taskId || generation.revision !== binding.revision || generation.policyDigest !== binding.policyDigest || generation.registrationDigest !== binding.registrationDigest || stableDigest(generation.assignments) !== binding.assignmentStoreDigest || stableDigest(generation.originRegistry) !== binding.originRegistryDigest) return report;
|
|
190
|
+
// A controller-linked immutable history is useful when current policy/pointer
|
|
191
|
+
// cannot be assessed. It never supplies current source, tasks or resolutions.
|
|
192
|
+
const result = evaluateBoundShipEvidence({expected: null, invocations: [], records: generation.records, obligations: generation.obligations, resolutions: [], tasks: [], readStatus: {version: 'ship-read-status/v1', nowMs: now(clock), context: 'UNKNOWN', assignments: 'UNKNOWN', journals: 'UNKNOWN', tasks: 'UNKNOWN', policy: 'UNKNOWN', origins: 'UNKNOWN', trustedOwnerDecisions: []}});
|
|
193
|
+
return {...report, revision: binding.revision, result};
|
|
194
|
+
} catch { return report; }
|
|
195
|
+
}
|
|
196
|
+
export function evaluateBoundGate({root, store, runId, expectedRevision}, {runStore, clock = Date.now, readTasks = null, readJournals = null} = {}) {
|
|
197
|
+
try {
|
|
198
|
+
root = safeStores(root, store, runStore);
|
|
199
|
+
if (!uuid(runId) || !positive(expectedRevision)) fail('SHIP_INVALID_REVISION');
|
|
200
|
+
const state = readJSON(join(runStore, `${runId}.json`)), b = readBinding(state);
|
|
201
|
+
if (b.revision !== expectedRevision) fail('SHIP_STALE_REVISION');
|
|
202
|
+
const descriptor = readShipPolicy({root, path: b.policyPath});
|
|
203
|
+
if (descriptor.store !== store) fail('SHIP_UNBOUND');
|
|
204
|
+
const gen = linked(state, descriptor, store), {result} = innerEvaluation(state, descriptor, gen, {clock, readTasks, readJournals});
|
|
205
|
+
return {version: 'ship-bound-gate/v1', state: result.state, contextDigest: b.contextDigest, revision: b.revision, result, publicationEligibility: 'NOT_CHECKED'};
|
|
206
|
+
} catch { return knownHistoricalFindings({root, store, runId, expectedRevision}, {runStore, clock}); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function assertShipControllerBoundary(boundary, state) {
|
|
210
|
+
const owned = brands.get(boundary);
|
|
211
|
+
if (!owned || owned.root !== state.root || owned.runId !== state.id || owned.taskId !== state.taskId) fail('SHIP_CAPABILITY_INVALID');
|
|
212
|
+
return boundary;
|
|
213
|
+
}
|
|
214
|
+
function makeBoundary({root, store, lease, readPersistedState, writeGeneration}, options, seed = null) {
|
|
215
|
+
const {runStore, clock = Date.now, readTasks = null, readJournals = null} = options;
|
|
216
|
+
root = safeStores(root, store, runStore);
|
|
217
|
+
if (!lease || typeof lease.assertOwned !== 'function') fail('SHIP_LEASE_LOST');
|
|
218
|
+
const assertLease = () => { try { lease.assertOwned(); } catch { fail('SHIP_LEASE_LOST'); } };
|
|
219
|
+
assertLease();
|
|
220
|
+
const read = readPersistedState || (runId => readJSON(join(runStore, `${runId}.json`)));
|
|
221
|
+
const write = writeGeneration || (({runId, generation, digest, bytes}) => {
|
|
222
|
+
const dir = evidenceDirectories(store, runId), path = join(dir, 'generations', `${generation}.json`);
|
|
223
|
+
if (existsSync(path)) {
|
|
224
|
+
privatePath(path, 'file'); if (sha(readFileSync(path)) !== digest) fail('SHIP_GENERATION_CONFLICT');
|
|
225
|
+
return {created: false, digest};
|
|
226
|
+
}
|
|
227
|
+
writeFileSync(path, bytes, {mode: 0o600, flag: 'wx'});
|
|
228
|
+
return {created: true, digest: sha(readFileSync(privatePath(path, 'file')))};
|
|
229
|
+
});
|
|
230
|
+
let runId = seed?.state.id || null, taskId = seed?.state.taskId || null, revision = seed ? 0 : null, gen = seed?.generation || null, descriptor = seed?.descriptor || null, priorDigest = null, poisoned = false;
|
|
231
|
+
const activeCapabilities = new Set(), callIds = new Set();
|
|
232
|
+
const load = state => {
|
|
233
|
+
if (poisoned) fail('SHIP_COMMITTED_OBSERVATION_UNKNOWN', {persistedRevision: revision});
|
|
234
|
+
assertLease();
|
|
235
|
+
if (!runId) {
|
|
236
|
+
const b = readBinding(state);
|
|
237
|
+
descriptor = readShipPolicy({root, path: b.policyPath});
|
|
238
|
+
if (descriptor.store !== store) fail('SHIP_UNBOUND');
|
|
239
|
+
const saved = read(state.id); if (saved?.then) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
240
|
+
gen = clone(linked(saved, descriptor, store)); revision = saved.shipEvidence.revision; priorDigest = saved.shipEvidence.generationDigest;
|
|
241
|
+
runId = saved.id; taskId = saved.taskId;
|
|
242
|
+
brands.set(boundary, {root, runId, taskId});
|
|
243
|
+
}
|
|
244
|
+
if (state.id !== runId || state.taskId !== taskId || realpathSync(state.root) !== root) fail('SHIP_CAPABILITY_INVALID');
|
|
245
|
+
if (revision === 0) return null;
|
|
246
|
+
const saved = read(runId); if (saved?.then) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
247
|
+
const b = readBinding(saved);
|
|
248
|
+
if (b.revision !== revision || state.shipEvidence?.revision !== revision || b.generationDigest !== priorDigest || state.shipEvidence.generationDigest !== priorDigest) fail('SHIP_STALE_REVISION');
|
|
249
|
+
const currentDescriptor = readShipPolicy({root, path: b.policyPath});
|
|
250
|
+
linked(saved, currentDescriptor, store);
|
|
251
|
+
if (currentDescriptor.policyDigest !== descriptor.policyDigest || currentDescriptor.registrationDigest !== descriptor.registrationDigest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
252
|
+
return saved;
|
|
253
|
+
};
|
|
254
|
+
const assertSource = () => {
|
|
255
|
+
if (gen.expected && recheckShipContext({root, registration: descriptor.registration, expected: gen.expected}).state !== 'MATCH') fail('SHIP_CONTEXT_DRIFT');
|
|
256
|
+
};
|
|
257
|
+
const persistBlob = value => {
|
|
258
|
+
const digest = stableDigest(value), dir = evidenceDirectories(store, runId), path = join(dir, 'blobs', `${digest}.json`), bytes = bytesOf(value);
|
|
259
|
+
if (sha(bytes) !== digest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
260
|
+
if (existsSync(path)) { privatePath(path, 'file'); if (sha(readFileSync(path)) !== digest) fail('SHIP_GENERATION_CONFLICT'); }
|
|
261
|
+
else writeFileSync(path, bytes, {flag: 'wx', mode: 0o600});
|
|
262
|
+
return digest;
|
|
263
|
+
};
|
|
264
|
+
const retainCallFinding = (cap, digest, category) => {
|
|
265
|
+
const ordinal = gen.obligations.length;
|
|
266
|
+
gen.obligations.push({findingId: `controller:${randomUUID()}`, originRecordDigest: digest, originContextDigest: cap.assignment.expectedContextDigest, role: cap.assignment.role, severity: 'UNKNOWN', category, origin: {journalId: `controller:${runId}`, ordinal, rawDigest: digest}});
|
|
267
|
+
};
|
|
268
|
+
const boundary = {
|
|
269
|
+
assertCurrent(state, {effect} = {}) {
|
|
270
|
+
if (!['metadata', 'worker', 'checks', 'verifier', 'ship', 'devops', 'release'].includes(effect)) fail('SHIP_CAPABILITY_INVALID');
|
|
271
|
+
load(state); assertSource();
|
|
272
|
+
const requiredRole = descriptor.requirements.some(r => r.role === (state.active || state.queue?.[0]));
|
|
273
|
+
if (effect !== 'metadata' && (requiredRole || ['ship', 'devops', 'release'].includes(effect)) && !['REVIEWING', 'READY'].includes(gen.phase)) fail('SHIP_REASSESS_REQUIRED');
|
|
274
|
+
if (['ship', 'devops', 'release'].includes(effect)) {
|
|
275
|
+
const report = evaluateBoundGate({root, store, runId, expectedRevision: revision}, {runStore, clock, readTasks, readJournals});
|
|
276
|
+
if (report.state !== 'PASS') fail(report.state === 'BLOCKED' ? 'SHIP_EVIDENCE_BLOCKED' : 'SHIP_EVIDENCE_UNKNOWN');
|
|
277
|
+
}
|
|
278
|
+
return {revision, contextDigest: gen.expected ? stableDigest(gen.expected) : null};
|
|
279
|
+
},
|
|
280
|
+
assertReady(state, {role, attemptId, host}) {
|
|
281
|
+
load(state);
|
|
282
|
+
if (!['REVIEWING', 'READY'].includes(gen.phase)) {
|
|
283
|
+
if (gen.phase === 'PROVISIONAL') { gen.phase = 'NEEDS_REASSESSMENT'; state.shipEvidence.phase = gen.phase; }
|
|
284
|
+
fail('SHIP_REASSESS_REQUIRED');
|
|
285
|
+
}
|
|
286
|
+
boundary.assertCurrent(state, {effect: 'worker'});
|
|
287
|
+
const required = gen.expected.requiredReviewers.find(r => r.role === role);
|
|
288
|
+
if (!required || !id(attemptId) || !['codex', 'claude-code'].includes(host) || !state.graph[role] || (state.hostRoutes?.[role] || 'codex') !== host) fail('SHIP_CAPABILITY_INVALID');
|
|
289
|
+
const issuedAtMs = now(clock), expiresAtMs = issuedAtMs + descriptor.policy.assignmentLifetimeMs;
|
|
290
|
+
if (!Number.isSafeInteger(expiresAtMs)) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
291
|
+
const reviewerActorId = invocationIdentity({session_id: runId, agent_id: attemptId});
|
|
292
|
+
if (!reviewerActorId) fail('SHIP_CAPABILITY_INVALID');
|
|
293
|
+
const assignment = {version: 'ship-assignment/v1', reviewerInvocationId: randomUUID(), role, expectedContextDigest: stableDigest(gen.expected), taskId, runId, reviewerActorId, independentOf: [...required.independentOf], requiredChecks: [...required.requiredChecks], issuedAtMs, expiresAtMs, revoked: false, origin: 'CONTROLLER'};
|
|
294
|
+
if (required.independentOf.includes(assignment.reviewerActorId)) fail('SHIP_CAPABILITY_INVALID');
|
|
295
|
+
gen.assignments.push(assignment);
|
|
296
|
+
const capability = Object.freeze({});
|
|
297
|
+
caps.set(capability, {boundary, assignment, state, attemptId, host, calls: new Map(), startedAtMs: issuedAtMs, completed: false});
|
|
298
|
+
activeCapabilities.add(capability); return capability;
|
|
299
|
+
},
|
|
300
|
+
async runCall(capability, {kind, callId, invoke}) {
|
|
301
|
+
const cap = caps.get(capability);
|
|
302
|
+
if (!cap || cap.boundary !== boundary || cap.completed || !activeCapabilities.has(capability) || cap.assignment.revoked || cap.assignment.expectedContextDigest !== (gen.expected && stableDigest(gen.expected))) fail('SHIP_CAPABILITY_INVALID');
|
|
303
|
+
if (!['worker', 'verifier', 'checks'].includes(kind) || !id(callId) || typeof invoke !== 'function') fail('SHIP_CAPABILITY_INVALID');
|
|
304
|
+
if (cap.calls.has(kind) || callIds.has(callId)) fail('SHIP_REPLAY');
|
|
305
|
+
boundary.assertCurrent(cap.state, {effect: kind});
|
|
306
|
+
callIds.add(callId); const call = {callId, result: null, digest: null, returned: false}; cap.calls.set(kind, call);
|
|
307
|
+
try {
|
|
308
|
+
const result = await invoke();
|
|
309
|
+
boundary.assertCurrent(cap.state, {effect: kind});
|
|
310
|
+
call.result = result; call.digest = stableDigest(result); call.returned = true;
|
|
311
|
+
if (kind === 'worker') {
|
|
312
|
+
let parsed; try { parsed = JSON.parse(result.finalText ?? result.text); } catch {}
|
|
313
|
+
if (result.state !== 'ok' || result.code !== 0 || result.errors?.length || !parsed || verdictKind(parsed.verdict) !== 'positive') retainCallFinding(cap, call.digest, 'worker-return');
|
|
314
|
+
}
|
|
315
|
+
if (kind === 'checks' && (result?.state !== 'passed' || result?.code !== 0)) retainCallFinding(cap, call.digest, 'checks-return');
|
|
316
|
+
return result;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
call.failed = true;
|
|
319
|
+
// This is an unresolved call finding, not a fabricated review completion.
|
|
320
|
+
retainCallFinding(cap, stableDigest({kind, callId, code: String(error.code || 'CALL_UNKNOWN')}), 'transport-unknown');
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
completeReview(state, {capability, proposal, verification, checks = null}) {
|
|
325
|
+
const cap = caps.get(capability);
|
|
326
|
+
if (!cap || cap.boundary !== boundary || cap.state !== state || cap.completed || !activeCapabilities.has(capability)) fail('SHIP_CAPABILITY_INVALID');
|
|
327
|
+
boundary.assertCurrent(state, {effect: 'verifier'});
|
|
328
|
+
const worker = cap.calls.get('worker'), verifier = cap.calls.get('verifier'), check = cap.calls.get('checks');
|
|
329
|
+
if (!worker?.returned || stableDigest(worker.result) !== worker.digest || !verifier?.returned || verifier.result !== verification || stableDigest(verification) !== verifier.digest || (checks !== null && (!check?.returned || check.result !== checks || stableDigest(checks) !== check.digest))) fail('SHIP_CAPABILITY_INVALID');
|
|
330
|
+
let parsed;
|
|
331
|
+
try { parsed = JSON.parse(worker.result.finalText ?? worker.result.text); } catch { fail('SHIP_CAPABILITY_INVALID'); }
|
|
332
|
+
if (stableDigest(parsed) !== stableDigest(proposal)) fail('SHIP_CAPABILITY_INVALID');
|
|
333
|
+
if (!Array.isArray(proposal.files) || proposal.files.length) fail('UNSUPPORTED_REVIEW_OUTPUT');
|
|
334
|
+
const rule = state.graph[`${cap.assignment.role}.${proposal.verdict}`] || state.graph[cap.assignment.role];
|
|
335
|
+
assertCommittedShipArtifacts({root, allowed: state.allowed, rule, proposal});
|
|
336
|
+
const workerClean = worker.result.state === 'ok' && worker.result.code === 0 && Array.isArray(worker.result.errors) && worker.result.errors.length === 0;
|
|
337
|
+
const verifierClean = verification.state === 'verified' && Array.isArray(verification.findings) && verification.findings.length === 0 && Array.isArray(verification.checks) && verification.checks.length > 0;
|
|
338
|
+
const checkEvidence = cap.assignment.requiredChecks.map(checkId => ({checkId, state: (checkId === 'independent-verifier' ? verifierClean : check?.returned && checks?.state === 'passed' && checks?.code === 0) ? 'PASS' : 'FAIL', evidenceDigest: checkId === 'independent-verifier' ? verifier.digest : check?.digest || stableDigest(null)}));
|
|
339
|
+
const outcome = workerClean && verifierClean && rule?.on?.includes(proposal.verdict) && checkEvidence.every(c => c.state === 'PASS') ? 'PASS' : ['FAIL', 'REJECTED', 'BLOCKED'].includes(proposal.verdict) ? 'FAIL' : 'UNKNOWN';
|
|
340
|
+
const recordId = randomUUID(), finishedAtMs = now(clock), contextDigest = stableDigest(gen.expected);
|
|
341
|
+
if (finishedAtMs >= cap.assignment.expiresAtMs) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
342
|
+
const envelope = {version: 'ship-completion/v1', assignmentDigest: stableDigest(cap.assignment), reviewerInvocationId: cap.assignment.reviewerInvocationId, attemptId: cap.attemptId, role: cap.assignment.role, host: cap.host, expectedContextDigest: contextDigest, sourceBeforeDigest: contextDigest, sourceAfterDigest: contextDigest, workerCallId: worker.callId, workerTransportDigest: worker.digest, proposalDigest: stableDigest(proposal), verifierCallId: verifier.callId, verifierTransportDigest: verifier.digest, verificationDigest: stableDigest(verification), checkEvidence, startedAtMs: cap.startedAtMs, finishedAtMs, outcome, recordId};
|
|
343
|
+
const completionDigest = persistBlob(envelope);
|
|
344
|
+
gen.records.push({version: 'ship-evidence/v2', recordId, reviewerInvocationId: cap.assignment.reviewerInvocationId, expectedContextDigest: contextDigest, role: cap.assignment.role, outcome, checkEvidence, reviewedEvidenceDigest: stableDigest({proposalDigest: envelope.proposalDigest, verificationDigest: envelope.verificationDigest}), authority: 'CONTROLLED_REVIEW', releaseEligibility: 'SOURCE_GATE_ONLY', evidenceClass: 'SOURCE_REVIEW', findingIds: [], startedAtMs: cap.startedAtMs, finishedAtMs, rawRecordDigest: completionDigest, origin: {journalId: `controller:${runId}`, ordinal: gen.records.length, rawDigest: completionDigest}});
|
|
345
|
+
gen.completionDigests.push(completionDigest); gen.completionDigests.sort(); cap.completed = true; activeCapabilities.delete(capability);
|
|
346
|
+
return {recordId, completionDigest, outcome};
|
|
347
|
+
},
|
|
348
|
+
assertApproval(state, {token}) {
|
|
349
|
+
const saved = load(state);
|
|
350
|
+
if (!saved?.pending || token !== saved.pending.token || token !== state.pending?.token) fail('SHIP_STALE_REVISION');
|
|
351
|
+
if (state.pending.gates.includes('gate:ship') && (state.pending.shipEvidenceRevision !== revision || state.pending.shipEvidenceContextDigest !== state.shipEvidence.contextDigest || saved.pending.shipEvidenceRevision !== revision || saved.pending.shipEvidenceContextDigest !== state.shipEvidence.contextDigest)) fail('SHIP_STALE_REVISION');
|
|
352
|
+
return boundary.assertCurrent(state, {effect: state.pending.gates.includes('gate:ship') ? 'ship' : 'metadata'});
|
|
353
|
+
},
|
|
354
|
+
save(state) {
|
|
355
|
+
const saved = load(state);
|
|
356
|
+
if (revision >= Number.MAX_SAFE_INTEGER) fail('SHIP_INVALID_REVISION');
|
|
357
|
+
privatePath(join(runStore, `${runId}.lock`), 'dir');
|
|
358
|
+
assertSource();
|
|
359
|
+
const protectedEffect = saved && (
|
|
360
|
+
state.approvals.some(a => a.gate === 'gate:ship' && !saved.approvals.some(old => stableDigest(old) === stableDigest(a)))
|
|
361
|
+
|| state.released.some(role => !saved.released.includes(role) && (state.graph[role]?.gate === 'gate:ship' || state.graph[role]?.next?.includes('devops')))
|
|
362
|
+
|| stableDigest(state.release || null) !== stableDigest(saved.release || null)
|
|
363
|
+
|| state.results.devops && !saved.results.devops);
|
|
364
|
+
const assertProtectedEffect = () => {
|
|
365
|
+
if (!protectedEffect) return;
|
|
366
|
+
const {result} = innerEvaluation(state, descriptor, gen, {clock, readTasks, readJournals});
|
|
367
|
+
if (result.state !== 'PASS') fail(result.state === 'BLOCKED' ? 'SHIP_EVIDENCE_BLOCKED' : 'SHIP_EVIDENCE_UNKNOWN');
|
|
368
|
+
};
|
|
369
|
+
assertProtectedEffect();
|
|
370
|
+
const next = clone(gen), stagedState = clone(state), nextRevision = revision + 1;
|
|
371
|
+
if (gen.expected) {
|
|
372
|
+
const evaluation = innerEvaluation(state, descriptor, gen, {clock, readTasks, readJournals}, {allowAppend: true});
|
|
373
|
+
next.originRegistry = evaluation.snaps.normalized.registry;
|
|
374
|
+
next.obligations = evaluation.result.openObligations.concat(evaluation.result.resolvedObligations, evaluation.result.supersededObligations);
|
|
375
|
+
next.readStatus = evaluation.snaps.status; next.taskSnapshotDigest = evaluation.snaps.taskSnapshotDigest; next.journalSnapshotDigest = evaluation.snaps.journalSnapshotDigest;
|
|
376
|
+
if (next.taskSnapshotDigest && persistBlob(evaluation.snaps.rawTasks.tasks) !== next.taskSnapshotDigest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
377
|
+
if (next.journalSnapshotDigest && persistBlob(evaluation.snaps.rawJournals.journals) !== next.journalSnapshotDigest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
378
|
+
persistBlob(evaluation.snaps.rawTasks); persistBlob(evaluation.snaps.rawJournals);
|
|
379
|
+
if (evaluation.result.state === 'PASS') next.phase = 'READY';
|
|
380
|
+
else if (next.phase === 'READY') next.phase = 'REVIEWING';
|
|
381
|
+
}
|
|
382
|
+
next.revision = nextRevision; next.previousGenerationDigest = revision === 0 ? null : priorDigest;
|
|
383
|
+
const digest = stableDigest(next), bytes = bytesOf(next);
|
|
384
|
+
if (sha(bytes) !== digest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
385
|
+
const response = write({runId, generation: nextRevision, digest, bytes});
|
|
386
|
+
if (response?.then || response?.digest !== digest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
387
|
+
const generationPath = join(runDirectory(store, runId), 'generations', `${nextRevision}.json`);
|
|
388
|
+
if (stableDigest(readJSON(generationPath)) !== digest || sha(readFileSync(generationPath)) !== digest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
389
|
+
const binding = {version: 'ship-bound-run/v1', revision: nextRevision, phase: next.phase, policyPath: descriptor.policyPath, policyDigest: descriptor.policyDigest, registrationDigest: descriptor.registrationDigest, storeDigest: sha(store), contextDigest: next.expected ? stableDigest(next.expected) : null, assignmentStoreDigest: stableDigest(next.assignments), originRegistryDigest: stableDigest(next.originRegistry), generationDigest: digest};
|
|
390
|
+
stagedState.shipEvidence = binding;
|
|
391
|
+
const isNewPending = stagedState.pending && (!saved?.pending || stagedState.pending.token !== saved.pending.token);
|
|
392
|
+
if (stagedState.pending?.gates.includes('gate:ship') && isNewPending) {
|
|
393
|
+
stagedState.pending.shipEvidenceRevision = nextRevision; stagedState.pending.shipEvidenceContextDigest = binding.contextDigest;
|
|
394
|
+
}
|
|
395
|
+
assertLease(); assertSource(); assertProtectedEffect();
|
|
396
|
+
const currentDescriptor = readShipPolicy({root, path: descriptor.policyPath});
|
|
397
|
+
if (currentDescriptor.policyDigest !== descriptor.policyDigest || currentDescriptor.registrationDigest !== descriptor.registrationDigest) fail('SHIP_EVIDENCE_UNKNOWN');
|
|
398
|
+
if (revision !== 0) {
|
|
399
|
+
const final = read(runId);
|
|
400
|
+
if (readBinding(final).revision !== revision || final.shipEvidence.generationDigest !== priorDigest) fail('SHIP_STALE_REVISION');
|
|
401
|
+
} else if (existsSync(join(runStore, `${runId}.json`))) fail('SHIP_STALE_REVISION');
|
|
402
|
+
const file = join(runStore, `${runId}.json`), temp = `${file}.${randomUUID()}.tmp`;
|
|
403
|
+
try { writeFileSync(temp, JSON.stringify(stagedState, null, 2), {flag: 'wx', mode: 0o600}); assertLease(); assertSource(); renameSync(temp, file); }
|
|
404
|
+
catch (error) { try { if (existsSync(temp)) unlinkSync(temp); } catch {} throw error; }
|
|
405
|
+
revision = nextRevision; priorDigest = digest; gen = next; state.shipEvidence = clone(binding);
|
|
406
|
+
if (state.pending && stagedState.pending) state.pending = clone(stagedState.pending);
|
|
407
|
+
try { writeFileSync(join(runDirectory(store, runId), 'current.json'), bytesOf({version: 'ship-store-pointer/v1', generation: revision, digest}), {mode: 0o600}); }
|
|
408
|
+
catch { poisoned = true; fail('SHIP_COMMITTED_OBSERVATION_UNKNOWN', {persistedRevision: revision}); }
|
|
409
|
+
return {revision, contextDigest: binding.contextDigest, generationDigest: digest, committed: true};
|
|
410
|
+
},
|
|
411
|
+
reassess(state, {expectedRevision}) {
|
|
412
|
+
load(state);
|
|
413
|
+
if (expectedRevision !== revision) fail('SHIP_STALE_REVISION');
|
|
414
|
+
if (state.active || state.wave || ['done', 'cancelled'].includes(state.status) || state.release && ['publishing', 'published', 'verified'].includes(state.release.status)) fail('SHIP_REASSESS_REQUIRED');
|
|
415
|
+
const freshDescriptor = readShipPolicy({root, path: descriptor.policyPath});
|
|
416
|
+
const requirements = requirementsFor(freshDescriptor, state);
|
|
417
|
+
for (const r of requirements) if (!state.graph[r.role] || (state.attempts || []).filter(a => a.role === r.role).length >= state.maxAttempts || r.requiredChecks.includes('configured-checks') && !state.checkPolicy) fail('SHIP_REQUIREMENTS_UNKNOWN');
|
|
418
|
+
const captured = captureShipContext({root, registration: descriptor.registration, taskId, runId, gateId: 'gate:ship', policy: descriptor.policy, requirements});
|
|
419
|
+
if (captured.state !== 'COMPLETE') fail('SHIP_CONTEXT_DRIFT');
|
|
420
|
+
const staged = clone(state), roles = new Set(requirements.map(r => r.role));
|
|
421
|
+
staged.invalidations ??= []; staged.invalidations.push({kind: 'ship-reassessment', results: Object.fromEntries(Object.entries(staged.results).filter(([r]) => roles.has(r))), approvals: staged.approvals.filter(a => roles.has(a.role)), at: new Date(now(clock)).toISOString()});
|
|
422
|
+
for (const role of roles) delete staged.results[role];
|
|
423
|
+
staged.approvals = staged.approvals.filter(a => !roles.has(a.role)); staged.released = staged.released.filter(r => !roles.has(r));
|
|
424
|
+
const orderedRoles = Object.keys(state.graph).filter(r => roles.has(r));
|
|
425
|
+
const first = orderedRoles[0], joined = state.graph[first]?.join || [];
|
|
426
|
+
staged.queue = [...new Set([first, ...joined.filter(r => roles.has(r))])]; staged.pending = null; staged.active = null; staged.status = 'ready'; delete staged.reason;
|
|
427
|
+
gen = {...gen, expected: captured.expected, phase: 'REVIEWING', assignments: gen.assignments.map(a => ({...a, revoked: true}))};
|
|
428
|
+
// New generations retain negative history; prior positive assignments cannot satisfy.
|
|
429
|
+
for (const cap of activeCapabilities) caps.get(cap).assignment.revoked = true;
|
|
430
|
+
activeCapabilities.clear();
|
|
431
|
+
const saved = boundary.save(staged); return {state: staged, revision: saved.revision, contextDigest: saved.contextDigest};
|
|
432
|
+
},
|
|
433
|
+
invalidate(state, {reason}) {
|
|
434
|
+
load(state); if (!id(reason)) fail('SHIP_CAPABILITY_INVALID');
|
|
435
|
+
gen.phase = 'INVALIDATED'; gen.expected = null; gen.assignments = gen.assignments.map(a => ({...a, revoked: true}));
|
|
436
|
+
for (const cap of activeCapabilities) caps.get(cap).assignment.revoked = true;
|
|
437
|
+
activeCapabilities.clear(); state.shipEvidence.phase = 'INVALIDATED'; state.shipEvidence.contextDigest = null;
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
brands.set(boundary, {root, runId, taskId});
|
|
441
|
+
// Existing bindings are linked lazily on their first state-bearing method.
|
|
442
|
+
const originalLoadBrand = state => { load(state); brands.set(boundary, {root, runId, taskId}); };
|
|
443
|
+
if (seed) brands.set(boundary, {root, runId, taskId});
|
|
444
|
+
else {
|
|
445
|
+
const assert = boundary.assertCurrent;
|
|
446
|
+
boundary.assertCurrent = (state, request) => { originalLoadBrand(state); return assert(state, request); };
|
|
447
|
+
}
|
|
448
|
+
return boundary;
|
|
449
|
+
}
|
|
450
|
+
export function createShipControllerBoundary(input, options = {}) { return makeBoundary(input, options); }
|
|
451
|
+
export function initializeShipBinding({state, policyPath, lease}, options = {}) {
|
|
452
|
+
if (state.shipEvidence || !uuid(state.id) || !uuid(state.taskId) || state.version !== 1) fail('SHIP_UNBOUND');
|
|
453
|
+
const descriptor = readShipPolicy({root: state.root, path: policyPath}), clock = options.clock || Date.now;
|
|
454
|
+
const generation = {version: 'ship-generation/v1', runId: state.id, taskId: state.taskId, revision: 1, phase: 'PROVISIONAL', policyDigest: descriptor.policyDigest, registrationDigest: descriptor.registrationDigest, expected: null, assignments: [], records: [], originRegistry: {version: 'ship-origin-registry/v1', journals: []}, obligations: [], resolutions: [], readStatus: {version: 'ship-read-status/v1', nowMs: now(clock), context: 'UNKNOWN', assignments: 'UNKNOWN', journals: 'UNKNOWN', tasks: 'UNKNOWN', policy: 'UNKNOWN', origins: 'UNKNOWN', trustedOwnerDecisions: []}, taskSnapshotDigest: null, journalSnapshotDigest: null, completionDigests: [], previousGenerationDigest: null};
|
|
455
|
+
const staged = clone(state), boundary = makeBoundary({root: state.root, store: descriptor.store, lease}, options, {state: staged, descriptor, generation});
|
|
456
|
+
boundary.save(staged); return {state: staged, boundary};
|
|
457
|
+
}
|
|
458
|
+
export function reassessShipBinding({state, expectedRevision, lease}, options = {}) {
|
|
459
|
+
const b = readBinding(state), descriptor = readShipPolicy({root: state.root, path: b.policyPath});
|
|
460
|
+
const boundary = createShipControllerBoundary({root: state.root, store: descriptor.store, lease}, options);
|
|
461
|
+
const result = boundary.reassess(state, {expectedRevision});
|
|
462
|
+
return {state: result.state, boundary};
|
|
463
|
+
}
|
|
464
|
+
function main() {
|
|
465
|
+
const args = process.argv.slice(2), values = {}, allowed = new Set(['--dir', '--store', '--run', '--revision']); let diagnostic = false;
|
|
466
|
+
try {
|
|
467
|
+
for (let i = 0; i < args.length; i++) {
|
|
468
|
+
const key = args[i]; if (key === '--json') continue;
|
|
469
|
+
if (key === '--as') { diagnostic = true; i++; continue; }
|
|
470
|
+
if (!allowed.has(key) || Object.hasOwn(values, key) || !args[i + 1] || args[i + 1].startsWith('--')) fail('SHIP_CAPABILITY_INVALID');
|
|
471
|
+
values[key] = args[++i];
|
|
472
|
+
}
|
|
473
|
+
if (!/^\d+$/.test(values['--revision'] || '')) fail('SHIP_INVALID_REVISION');
|
|
474
|
+
const report = evaluateBoundGate({root: values['--dir'], store: values['--store'], runId: values['--run'], expectedRevision: Number(values['--revision'])}, {runStore: codexRunStore()});
|
|
475
|
+
console.log(JSON.stringify(report)); process.exitCode = report.state === 'PASS' && !diagnostic ? 0 : 2;
|
|
476
|
+
} catch { console.log(JSON.stringify(unknownReport())); process.exitCode = 2; }
|
|
477
|
+
}
|
|
478
|
+
if (isMainModule(import.meta.url)) main();
|