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
|
@@ -24,6 +24,35 @@ export const AGENT_PIPELINE = [
|
|
|
24
24
|
{ name: 'devops', phase: 'deploy', group: 'deploy', parallel: false },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
const V2_PLANNING_PREDECESSORS = Object.freeze({
|
|
28
|
+
brief: Object.freeze(['work-understanding']),
|
|
29
|
+
detail: Object.freeze(['work-understanding', 'brief']),
|
|
30
|
+
architect: Object.freeze(['work-understanding', 'brief']),
|
|
31
|
+
ux: Object.freeze(['work-understanding', 'brief']),
|
|
32
|
+
'qa-planning': Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux']),
|
|
33
|
+
phases: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning']),
|
|
34
|
+
tasks: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning', 'phases']),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Shared by reservation, command consumption and canonical advance. Keep this
|
|
38
|
+
// check pure so the session writer can apply the same contract under its lock.
|
|
39
|
+
export function assertV2PlanningPredecessors(session, agent) {
|
|
40
|
+
const projectType = session?.project_type || session?.project?.type || 'greenfield';
|
|
41
|
+
const required = (V2_PLANNING_PREDECESSORS[agent] || []).map((name) =>
|
|
42
|
+
name === 'work-understanding' ? (projectType === 'brownfield' ? 'brownfield-wu' : 'greenfield-wu') : name
|
|
43
|
+
);
|
|
44
|
+
const incomplete = required.filter((name) =>
|
|
45
|
+
session?.agents?.[name]?.status !== 'completed' || !session?.completed_agents?.includes(name)
|
|
46
|
+
);
|
|
47
|
+
if (incomplete.length > 0) {
|
|
48
|
+
const error = new Error(`Agent ${agent} cannot execute or advance before its canonical predecessors complete: ${incomplete.join(', ')}`);
|
|
49
|
+
error.code = 'V2_PLANNING_PREDECESSORS_INCOMPLETE';
|
|
50
|
+
error.incomplete = incomplete;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({ valid: true, required: Object.freeze(required) });
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
/**
|
|
28
57
|
* Get agent definition by name.
|
|
29
58
|
*
|
|
@@ -197,9 +226,10 @@ export function selectAgent(context) {
|
|
|
197
226
|
*
|
|
198
227
|
* @param {string} agentName - Current agent
|
|
199
228
|
* @param {string[]} completedAgents - Already completed
|
|
229
|
+
* @param {object} [options] - V2-only recovery; legacy workflows retain forward selection
|
|
200
230
|
* @returns {{ next: string|null, isParallel: boolean, group: string[] }}
|
|
201
231
|
*/
|
|
202
|
-
export function getNextAgent(agentName, completedAgents = []) {
|
|
232
|
+
export function getNextAgent(agentName, completedAgents = [], { recoverPlanningPeers = false } = {}) {
|
|
203
233
|
const currentDef = getAgentDef(agentName);
|
|
204
234
|
if (!currentDef) {
|
|
205
235
|
return { next: null, isParallel: false, group: [] };
|
|
@@ -207,6 +237,14 @@ export function getNextAgent(agentName, completedAgents = []) {
|
|
|
207
237
|
|
|
208
238
|
const currentIndex = AGENT_PIPELINE.indexOf(currentDef);
|
|
209
239
|
|
|
240
|
+
// Planning peers may finish in any order. The last completed member is not
|
|
241
|
+
// a watermark for the whole group, including after a legacy downstream cursor.
|
|
242
|
+
if (recoverPlanningPeers && (currentDef.phase === 'plan' || agentName === 'brief') && completedAgents.includes('brief')) {
|
|
243
|
+
const peers = getAgentsByGroup('plan-parallel');
|
|
244
|
+
const unfinished = peers.find(({ name }) => !completedAgents.includes(name));
|
|
245
|
+
if (unfinished) return { next: unfinished.name, isParallel: true, group: peers.map(({ name }) => name) };
|
|
246
|
+
}
|
|
247
|
+
|
|
210
248
|
// Look ahead for next incomplete agent
|
|
211
249
|
for (let i = currentIndex + 1; i < AGENT_PIPELINE.length; i++) {
|
|
212
250
|
const nextDef = AGENT_PIPELINE[i];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { canonicalSerialize, sha256 } from '@chati/core';
|
|
4
|
+
import { installationAuthorityProjectBinding, signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
|
|
5
|
+
import { listSafeInstallationStorageFiles, persistImmutableInstallationFile, readSafeInstallationStorageFile, removeSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
|
|
6
|
+
|
|
7
|
+
export const LOCAL_BROWSER_ARTIFACT_PREFIX = 'rail-browser-artifact://sha256/';
|
|
8
|
+
const DOMAIN = 'chati.browser-artifact-retention.v1';
|
|
9
|
+
const SEMANTICS = 'logical-ttl-opportunistic-purge-v1';
|
|
10
|
+
const RETENTION = Object.freeze({ 'internal-30d': 30 * 24 * 60 * 60 * 1000 });
|
|
11
|
+
const META_FIELDS = ['schema_version', 'artifact_id', 'run_id', 'project_id', 'project_binding', 'attempt_id',
|
|
12
|
+
'commit_ref', 'policy_ref', 'browser_session_id', 'kind', 'content_sha256', 'retention_class',
|
|
13
|
+
'retention_semantics', 'captured_at', 'expires_at'];
|
|
14
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
15
|
+
function exact(value, fields) {
|
|
16
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
&& Object.keys(value).length === fields.length && Object.keys(value).every((key) => fields.includes(key));
|
|
18
|
+
}
|
|
19
|
+
function now(clock) {
|
|
20
|
+
const value = new Date(clock()).getTime();
|
|
21
|
+
if (!Number.isFinite(value)) fail('BROWSER_RETENTION_TIME_INVALID', 'browser retention clock must be valid');
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function directory(projectDir) { return join(resolve(projectDir), '.chati/v2/rail/browser-artifacts'); }
|
|
25
|
+
function pathFor(projectDir, ref) {
|
|
26
|
+
if (typeof ref !== 'string' || !/^rail-browser-artifact:\/\/sha256\/[a-f0-9]{64}$/.test(ref)) fail('BROWSER_ARTIFACT_REF_INVALID', 'expected a content-addressed local browser artifact reference');
|
|
27
|
+
return join(directory(projectDir), `${ref.slice(LOCAL_BROWSER_ARTIFACT_PREFIX.length)}.json`);
|
|
28
|
+
}
|
|
29
|
+
export function browserRetentionPolicy(retentionClass) {
|
|
30
|
+
if (!Object.hasOwn(RETENTION, retentionClass)) fail('BROWSER_RETENTION_UNSUPPORTED', 'this local store supports internal-30d logical TTL only; physical-deletion guarantees require a different backend');
|
|
31
|
+
return Object.freeze({ retention_class: retentionClass, semantics: SEMANTICS, duration_ms: RETENTION[retentionClass] });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertRedactedContent(content) {
|
|
35
|
+
if (!exact(content, ['schema_version', 'projection', 'nodes']) || content.schema_version !== 1
|
|
36
|
+
|| content.projection !== 'dom-structure-all-values-redacted' || !Array.isArray(content.nodes)
|
|
37
|
+
|| content.nodes.length === 0 || content.nodes.length > 5000) fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'only the supported all-values-redacted DOM projection can enter this store');
|
|
38
|
+
const tags = new Set('HTML HEAD BODY TITLE META LINK STYLE SCRIPT DIV SPAN P H1 H2 H3 H4 H5 H6 A BUTTON INPUT FORM LABEL SELECT OPTION TEXTAREA UL OL LI TABLE THEAD TBODY TR TH TD MAIN NAV HEADER FOOTER SECTION ARTICLE ASIDE IMG SVG PATH CANVAS VIDEO AUDIO IFRAME BR HR STRONG EM B I PRE CODE #text #document OTHER'.split(' '));
|
|
39
|
+
for (const node of content.nodes) {
|
|
40
|
+
if (!exact(node, ['document', 'index', 'parent', 'type', 'tag', ...(node?.type === 3 ? ['text'] : [])])
|
|
41
|
+
|| !['document', 'index', 'parent', 'type'].every((key) => Number.isInteger(node[key]))
|
|
42
|
+
|| node.document < 0 || node.index < 0 || node.parent < -1 || node.type < 1 || node.type > 12
|
|
43
|
+
|| !tags.has(node.tag) || (node.type === 3 && node.text !== '[REDACTED]')) fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'DOM structure contains unsupported or unredacted fields');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function authenticatedRecord({ projectDir, projectId, ref, authorityDirectory }) {
|
|
48
|
+
const path = pathFor(projectDir, ref);
|
|
49
|
+
const source = readSafeInstallationStorageFile(path, { allowMissing: true, label: 'retained browser artifact', maxBytes: 2 * 1024 * 1024 });
|
|
50
|
+
if (!source) fail('BROWSER_ARTIFACT_MISSING', 'retained browser artifact is missing');
|
|
51
|
+
if (source.stat.uid !== BigInt(process.geteuid()) || (source.stat.mode & 0o777n) !== 0o600n) fail('BROWSER_ARTIFACT_STORAGE_UNSAFE', 'retained browser artifact must be private and owned by the current account');
|
|
52
|
+
let record;
|
|
53
|
+
try { record = JSON.parse(source.content); } catch { fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact is not valid JSON'); }
|
|
54
|
+
const metadata = record?.metadata;
|
|
55
|
+
if (!exact(record, ['metadata', 'content', 'authority_receipt']) || !exact(metadata, META_FIELDS)
|
|
56
|
+
|| metadata.schema_version !== 1 || metadata.kind !== 'dom' || metadata.project_id !== projectId
|
|
57
|
+
|| metadata.project_binding !== sha256(installationAuthorityProjectBinding(projectDir, projectId))
|
|
58
|
+
|| ref !== `${LOCAL_BROWSER_ARTIFACT_PREFIX}${sha256(metadata)}`
|
|
59
|
+
|| metadata.content_sha256 !== sha256(record.content)
|
|
60
|
+
|| metadata.retention_semantics !== SEMANTICS
|
|
61
|
+
|| !exact(record.authority_receipt, ['authority_id', 'signature'])
|
|
62
|
+
|| !verifyHostAuthorityPayload(metadata, { domain: DOMAIN, authorityDirectory,
|
|
63
|
+
authorityId: record.authority_receipt.authority_id, signature: record.authority_receipt.signature })) {
|
|
64
|
+
fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact metadata, bytes or host authentication changed');
|
|
65
|
+
}
|
|
66
|
+
const policy = browserRetentionPolicy(metadata.retention_class);
|
|
67
|
+
const captured = Date.parse(metadata.captured_at); const expires = Date.parse(metadata.expires_at);
|
|
68
|
+
if (!Number.isFinite(captured) || !Number.isFinite(expires) || expires - captured !== policy.duration_ms
|
|
69
|
+
|| new Date(captured).toISOString() !== metadata.captured_at || new Date(expires).toISOString() !== metadata.expires_at) fail('BROWSER_ARTIFACT_INVALID', 'retained browser artifact has an invalid fixed lifetime');
|
|
70
|
+
assertRedactedContent(record.content);
|
|
71
|
+
return { path, source, record, expires };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fixed lifetime from capture. Reading/retrying never modifies this record. */
|
|
75
|
+
export function readBrowserArtifact({ projectDir, projectId, ref, clock = () => new Date(), authorityDirectory } = {}) {
|
|
76
|
+
const loaded = authenticatedRecord({ projectDir, projectId, ref, authorityDirectory });
|
|
77
|
+
if (now(clock) >= loaded.expires) {
|
|
78
|
+
let purgeError;
|
|
79
|
+
try { removeSafeInstallationStorageFile(loaded.path, loaded.source); } catch (error) { purgeError = error.code || error.name; }
|
|
80
|
+
const error = Object.assign(new Error(`browser artifact expired${purgeError ? `; physical purge failed: ${purgeError}` : ''}`), { code: 'BROWSER_ARTIFACT_EXPIRED' });
|
|
81
|
+
if (purgeError) error.purge_error = purgeError;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze({ ref, ...loaded.record });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function persistBrowserArtifact({ projectDir, projectId, run_id, attempt_id, commit_ref, policy_ref,
|
|
88
|
+
browser_session_id, artifact, retention_class, captured_at, clock = () => new Date(), authorityDirectory } = {}) {
|
|
89
|
+
const policy = browserRetentionPolicy(retention_class);
|
|
90
|
+
if (!artifact || artifact.kind !== 'dom' || artifact.redaction_status !== 'redacted') fail('BROWSER_ARTIFACT_CONTENT_INVALID', 'artifact must be an actually redacted DOM observation');
|
|
91
|
+
assertRedactedContent(artifact.content);
|
|
92
|
+
const captured = Date.parse(captured_at);
|
|
93
|
+
if (!Number.isFinite(captured) || new Date(captured).toISOString() !== captured_at || captured > now(clock)
|
|
94
|
+
|| now(clock) >= captured + policy.duration_ms) fail('BROWSER_ARTIFACT_CAPTURE_INVALID', 'artifact capture must be current, never future or already expired');
|
|
95
|
+
if (![run_id, attempt_id, commit_ref, policy_ref, browser_session_id, projectId].every((value) => typeof value === 'string' && value.trim())) fail('BROWSER_ARTIFACT_BINDING_REQUIRED', 'retained artifact requires canonical project, attempt, result and browser provenance');
|
|
96
|
+
const metadata = { schema_version: 1, artifact_id: `browser-artifact-${randomUUID()}`, run_id, project_id: projectId,
|
|
97
|
+
project_binding: sha256(installationAuthorityProjectBinding(projectDir, projectId)), attempt_id, commit_ref,
|
|
98
|
+
policy_ref, browser_session_id, kind: 'dom', content_sha256: sha256(artifact.content), retention_class,
|
|
99
|
+
retention_semantics: SEMANTICS, captured_at, expires_at: new Date(captured + policy.duration_ms).toISOString() };
|
|
100
|
+
const ref = `${LOCAL_BROWSER_ARTIFACT_PREFIX}${sha256(metadata)}`;
|
|
101
|
+
const record = { metadata, content: artifact.content,
|
|
102
|
+
authority_receipt: signHostAuthorityPayload(metadata, { domain: DOMAIN, authorityDirectory }) };
|
|
103
|
+
persistImmutableInstallationFile(pathFor(projectDir, ref), `${canonicalSerialize(record)}\n`, { label: 'retained browser artifact', maxBytes: 2 * 1024 * 1024 });
|
|
104
|
+
readBrowserArtifact({ projectDir, projectId, ref, clock, authorityDirectory });
|
|
105
|
+
return Object.freeze({ artifact_id: metadata.artifact_id, kind: 'dom', sanitized_ref: ref,
|
|
106
|
+
redaction_status: 'redacted', retention_class, content_sha256: metadata.content_sha256 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function pruneBrowserArtifacts({ projectDir, projectId, clock = () => new Date(), authorityDirectory } = {}) {
|
|
110
|
+
const timestamp = now(clock); const removed = []; const failures = [];
|
|
111
|
+
for (const name of listSafeInstallationStorageFiles(directory(projectDir))) {
|
|
112
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
113
|
+
const ref = `${LOCAL_BROWSER_ARTIFACT_PREFIX}${name.slice(0, -5)}`;
|
|
114
|
+
try {
|
|
115
|
+
const loaded = authenticatedRecord({ projectDir, projectId, ref, authorityDirectory });
|
|
116
|
+
if (timestamp >= loaded.expires && removeSafeInstallationStorageFile(loaded.path, loaded.source)) removed.push(ref);
|
|
117
|
+
} catch (error) { failures.push({ ref, code: error.code || error.name }); }
|
|
118
|
+
}
|
|
119
|
+
return Object.freeze({ ok: failures.length === 0, removed, failures });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function assertLocalBrowserArtifacts({ projectDir, projectId, artifacts, expected, clock = () => new Date() }) {
|
|
123
|
+
for (const artifact of artifacts) {
|
|
124
|
+
if (!artifact.sanitized_ref.startsWith(LOCAL_BROWSER_ARTIFACT_PREFIX)) continue;
|
|
125
|
+
const { metadata } = readBrowserArtifact({ projectDir, projectId, ref: artifact.sanitized_ref, clock });
|
|
126
|
+
if (metadata.content_sha256 !== artifact.content_sha256 || metadata.retention_class !== artifact.retention_class
|
|
127
|
+
|| metadata.artifact_id !== artifact.artifact_id || metadata.kind !== artifact.kind
|
|
128
|
+
|| !Object.entries(expected).every(([key, value]) => metadata[key] === value)) fail('BROWSER_ARTIFACT_BINDING_MISMATCH', 'local browser artifact does not match certified execution and policy provenance');
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|