@milpa/comal 0.1.6 → 0.2.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/README.md +19 -1
- package/backend/contracts/remote-commands-v1.json +12 -0
- package/backend/contracts/remote-workspace-v1.schema.json +287 -0
- package/governance/chrome-task-v10.json +323 -0
- package/governance/chrome-task-v11.json +355 -0
- package/governance/chrome-task-v12.json +387 -0
- package/governance/chrome-task-v13.json +419 -0
- package/governance/chrome-task-v3.json +99 -0
- package/governance/chrome-task-v4.json +131 -0
- package/governance/chrome-task-v5.json +163 -0
- package/governance/chrome-task-v6.json +195 -0
- package/governance/chrome-task-v7.json +227 -0
- package/governance/chrome-task-v8.json +259 -0
- package/governance/chrome-task-v9.json +291 -0
- package/governance/source-task-v1.json +1 -0
- package/governance/source-task-v2.json +1 -0
- package/package.json +15 -1
- package/src/agent-recovery-proof.mjs +12 -1
- package/src/agent.mjs +94 -30
- package/src/audit.mjs +21 -6
- package/src/auto-guard.mjs +48 -1
- package/src/chrome-candidate-recovery-proof.mjs +47 -0
- package/src/chrome-recovery-proof.mjs +6 -2
- package/src/chrome-source-syntax.mjs +93 -0
- package/src/cli.mjs +161 -56
- package/src/codex-provider.mjs +3 -2
- package/src/connection-settings.mjs +204 -0
- package/src/context-pressure.mjs +2 -1
- package/src/durable-agent-runs.mjs +5 -11
- package/src/event-channels.mjs +8 -0
- package/src/file-recovery-proof.mjs +11 -3
- package/src/file-recovery.mjs +24 -0
- package/src/gemini-provider.mjs +6 -1
- package/src/git-agent-revision.mjs +12 -0
- package/src/git-agent-workspaces.mjs +143 -0
- package/src/git-integration-lineage.mjs +20 -0
- package/src/git-integration-operations.mjs +200 -0
- package/src/git-integration-recovery-proof.mjs +70 -0
- package/src/git-page-cli-bridge.mjs +61 -0
- package/src/git-page-evidence.mjs +30 -0
- package/src/git-page-identity.mjs +10 -0
- package/src/git-page-proof.mjs +96 -0
- package/src/git-page-settlement.mjs +21 -0
- package/src/git-page-verification.mjs +134 -0
- package/src/git-plumbing.mjs +212 -0
- package/src/git-process-worker.mjs +50 -0
- package/src/git-source.mjs +110 -0
- package/src/git-workspaces.mjs +519 -0
- package/src/goals.mjs +13 -7
- package/src/house.mjs +246 -20
- package/src/i18n/cli-es.mjs +4 -0
- package/src/i18n/es.mjs +35 -0
- package/src/judges.mjs +27 -11
- package/src/mcp-runtime.mjs +15 -3
- package/src/model-controls.mjs +11 -1
- package/src/model-deadline.mjs +4 -3
- package/src/native-refusal.mjs +38 -0
- package/src/onboarding.mjs +88 -24
- package/src/openai-provider.mjs +14 -7
- package/src/operations.mjs +9 -2
- package/src/output-recovery.mjs +48 -0
- package/src/peer-tools.mjs +18 -8
- package/src/permission-controls.mjs +232 -0
- package/src/permission-rules.mjs +312 -0
- package/src/permissions.mjs +113 -30
- package/src/plugin-catalog.mjs +84 -0
- package/src/plugin-config.mjs +48 -6
- package/src/plugin-controls.mjs +332 -0
- package/src/plugin-notices.mjs +88 -0
- package/src/plugin-trust.mjs +104 -0
- package/src/plugins/chrome-lab/bridge.mjs +38 -12
- package/src/plugins/chrome-lab/client.mjs +103 -25
- package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
- package/src/plugins/chrome-lab/index.mjs +116 -26
- package/src/plugins/chrome-lab/project.mjs +30 -2
- package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
- package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
- package/src/plugins/open-relay/actions.mjs +205 -0
- package/src/plugins/open-relay/bound.mjs +47 -0
- package/src/plugins/open-relay/client.mjs +158 -0
- package/src/plugins/open-relay/dossier.mjs +119 -0
- package/src/plugins/open-relay/index.mjs +153 -0
- package/src/plugins/open-relay/lookup.mjs +45 -0
- package/src/plugins/open-relay/notices.mjs +79 -0
- package/src/plugins/open-relay/watch.mjs +127 -0
- package/src/plugins.mjs +72 -29
- package/src/private-fs.mjs +40 -0
- package/src/process-lock.mjs +24 -13
- package/src/provider-auth.mjs +14 -3
- package/src/provider.mjs +11 -2
- package/src/read-only-check.mjs +7 -3
- package/src/remote-control.mjs +29 -4
- package/src/remote-workspace.mjs +50 -0
- package/src/scope-ceiling.mjs +56 -0
- package/src/scoped-policy.mjs +39 -0
- package/src/secret-login.mjs +68 -0
- package/src/session-bus.mjs +41 -7
- package/src/session-projections-candidate.mjs +119 -0
- package/src/session-projections.mjs +119 -0
- package/src/session-read-cache.mjs +23 -3
- package/src/sessions.mjs +35 -11
- package/src/source-editor-role.mjs +13 -0
- package/src/source-ownership.mjs +73 -0
- package/src/source-proposal-proof.mjs +55 -0
- package/src/source-proposals.mjs +118 -0
- package/src/source-requests.mjs +148 -0
- package/src/source-task-proof.mjs +24 -0
- package/src/source-task-revision-proof.mjs +44 -0
- package/src/subagents.mjs +187 -87
- package/src/tool-packs.mjs +5 -5
- package/src/tools.mjs +82 -15
- package/src/trusted-context.mjs +5 -1
- package/src/tui/connection-transition.mjs +1 -1
- package/src/tui/controller.mjs +8 -4
- package/src/tui/discovery.mjs +24 -12
- package/src/tui/inbound-blocks.mjs +55 -0
- package/src/tui/present.mjs +504 -0
- package/src/tui/screen.mjs +49 -13
- package/src/windows-private.ps1 +61 -0
- package/src/work-assertions.mjs +6 -2
- package/src/work-plan.mjs +34 -8
- package/src/work-state.mjs +15 -9
- package/src/workflows/chrome-page-cases.mjs +156 -0
- package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
- package/src/workflows/chrome-page-diagnostics.mjs +113 -0
- package/src/workflows/chrome-page-law.mjs +48 -0
- package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
- package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
- package/src/workflows/chrome-page-task.mjs +429 -0
- package/src/workflows/chrome-page-verifier.mjs +123 -0
- package/src/workflows/chrome-source.mjs +73 -0
- package/src/workflows/chrome-supplied-inputs.mjs +43 -0
- package/src/workflows/chrome-task-adoption.mjs +122 -0
- package/src/workflows/chrome-task.mjs +379 -74
- package/src/workflows/source-task-v1.mjs +124 -0
- package/src/workflows/source-task-v2.mjs +227 -0
- package/src/workflows/source-task.mjs +13 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, realpath } from 'node:fs/promises';
|
|
1
|
+
import { readFile, realpath, stat, lstat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import Ajv from 'ajv';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -6,27 +6,37 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
6
6
|
import { compileGovernance } from '../governance.mjs';
|
|
7
7
|
import { digest, immutable } from '../operations.mjs';
|
|
8
8
|
import { registerWorkOperations, workProjection } from '../work-plan.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { CHROME_SOURCE_NAMES, bindChromeSource } from './chrome-source.mjs';
|
|
10
|
+
import { PAGE_PREVIEW_RECOVERY_V13 } from './chrome-page-recovery-v13.mjs';
|
|
11
|
+
import { suppliedInputsSchema, normalizeSuppliedInputs, verifySuppliedInputs, isSuppliedPath, assertSuppliedSnapshot } from './chrome-supplied-inputs.mjs';
|
|
12
|
+
import { safeRelative, snapshotProject, chromeProjectNameSchema } from '../plugins/chrome-lab/project.mjs';
|
|
10
13
|
|
|
11
14
|
const prefix = 'plugin__chrome-lab__';
|
|
12
|
-
const PUBLIC = new Set(['Read', 'Edit', 'Write', 'ChromePublish', 'ChromeVerify', 'ChromeTest', 'ChromeFinish', 'ChromeRecover']);
|
|
13
|
-
const OWN = ['ChromePublish', 'ChromeVerify', 'ChromeTest', 'ChromeFinish', 'ChromeRecover'];
|
|
15
|
+
const PUBLIC = new Set(['Read', 'Edit', 'Write', 'ChromePublish', 'ChromeVerify', 'ChromeTest', 'ChromeFinish', 'ChromeRecover', 'ChromeInspect', 'ChromeDeliver']);
|
|
16
|
+
const OWN = ['ChromePublish', 'ChromeVerify', 'ChromeTest', 'ChromeFinish', 'ChromeRecover', 'ChromeInspect', 'ChromeDeliver'];
|
|
14
17
|
const INTERNAL = new Set(['WorkStepRun', 'WorkTaskUpdate']);
|
|
15
18
|
const empty = { type: 'object', properties: {}, additionalProperties: false };
|
|
16
19
|
const failure = (message, code = 'CHROME_TASK_CONFLICT') => Object.assign(new Error(message), { code });
|
|
17
|
-
const
|
|
18
|
-
|
|
20
|
+
export const chromeTaskSchema = { type: 'object', additionalProperties: false, required: ['project','directory'], properties: {
|
|
21
|
+
surface: { const: 'page' },
|
|
22
|
+
publication_recovery: {const:PAGE_PREVIEW_RECOVERY_V13},
|
|
23
|
+
verification: { type: 'object', required: ['implementation_sha256', 'contract_digest', 'criteria_digest', 'criteria'], additionalProperties: false, properties: { implementation_sha256: { type: 'string', pattern: '^[a-f0-9]{64}$' }, contract_digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, criteria_digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, criteria: { type: 'object' }, cases: { type: 'array', minItems: 1, maxItems: 24, uniqueItems: true, items: { type: 'string', pattern: '^[a-z][a-z0-9_-]{0,95}$' } } } },
|
|
24
|
+
project: chromeProjectNameSchema,
|
|
19
25
|
directory: { type: 'string', minLength: 1, maxLength: 220 }, popup: { type: 'string', minLength: 1, maxLength: 220 },
|
|
20
26
|
selector: { type: 'string', minLength: 1, maxLength: 1000 }, equals: { type: 'string', maxLength: 4000 },
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
fixture: {type:'object',additionalProperties:false,required:['file','selector','equals'],properties:{file:{type:'string',minLength:1,maxLength:220},selector:{type:'string',minLength:1,maxLength:1000},equals:{type:'string',maxLength:2000}},description:'An existing immutable HTML test page served on Win11 localhost. The house binds its exact target and checks content-script behavior; residents cannot edit the fixture.'},
|
|
28
|
+
supplied_inputs: suppliedInputsSchema,
|
|
29
|
+
baseline: {type:'boolean',description:'Only when the user requests an initial observation before edits. The house publishes and observes the accepted initial bytes first; this grants no permission.'},
|
|
30
|
+
tests: { type: 'array', minItems: 1, maxItems: 32, items: { type: 'object', additionalProperties: false, required: ['action'], properties: {
|
|
31
|
+
surface:{enum:['popup','page']}, action: { enum: ['click','fill','reload','assert','message'] }, request:{type:'string',minLength:1,maxLength:128}, selector: { type: 'string', minLength: 1, maxLength: 1000 }, text: { type: 'string', maxLength: 2000 }, equals: { type: 'string', maxLength: 2000 }, count: { type: 'integer', minimum: 0, maximum: 100 }
|
|
23
32
|
}, allOf: [
|
|
24
33
|
{ if: { properties: { action: { enum: ['click','fill','assert'] } } }, then: { required: ['selector'] } },
|
|
25
34
|
{ if: { properties: { action: { const: 'fill' } } }, then: { required: ['text'] } },
|
|
35
|
+
{ if: { properties: { action: { const: 'message' } } }, then: { required: ['request','equals'] } },
|
|
26
36
|
{ if: { properties: { action: { const: 'assert' } } }, then: { anyOf: [{ required: ['equals'] }, { required: ['count'] }] } }
|
|
27
37
|
] } }
|
|
28
|
-
} };
|
|
29
|
-
const validate = new Ajv({ strict: false }).compile(
|
|
38
|
+
}, allOf: [{ if: { required: ['surface'], properties: { surface: { const: 'page' } } }, then: { required: ['fixture', 'supplied_inputs', 'verification'], not: { anyOf: ['popup', 'selector', 'equals', 'tests', 'baseline'].map(key => ({ required: [key] })) } }, else: { required: ['popup', 'selector', 'equals'], not: { anyOf:[{required:['verification']},{required:['publication_recovery']}] } } }] };
|
|
39
|
+
const validate = new Ajv({ strict: false }).compile(chromeTaskSchema);
|
|
30
40
|
const manifestHash = snapshot => digest(snapshot.files.map(({ path, sha256 }) => ({ path, sha256 })));
|
|
31
41
|
export const savedChromeTask = session => session.rows.findLast(row => row.type === 'chrome_task_accepted' && row.source === 'workflow_host');
|
|
32
42
|
export async function readChromeTask(file) {
|
|
@@ -34,32 +44,81 @@ export async function readChromeTask(file) {
|
|
|
34
44
|
let task; try { task = JSON.parse(bytes); } catch { throw failure('Chrome task must be JSON.'); }
|
|
35
45
|
return normalizeTask(task);
|
|
36
46
|
}
|
|
37
|
-
function normalizeTask(task) {
|
|
47
|
+
export function normalizeTask(task) {
|
|
48
|
+
if (Buffer.byteLength(JSON.stringify(task)) > 16384) throw failure('Chrome task exceeds 16 KiB. Keep the contract bounded; preserve its functional criteria.');
|
|
38
49
|
if (!validate(task)) throw failure('Invalid Chrome task: '+validate.errors.map(e => e.instancePath+' '+e.message).join('; '));
|
|
39
|
-
const value = structuredClone(task); value.directory = safeRelative(value.directory); value.popup = safeRelative(value.popup);
|
|
50
|
+
const value = structuredClone(task); value.directory = safeRelative(value.directory); if(value.popup)value.popup = safeRelative(value.popup); if(value.fixture)value.fixture.file=safeRelative(value.fixture.file);if(value.supplied_inputs)normalizeSuppliedInputs(value.supplied_inputs); if(value.tests?.some(t=>t.surface==='page')&&!value.fixture)throw failure('Page steps require an immutable fixture.'); if(value.tests?.some(t=>t.surface==='page'&&t.action==='message'))throw failure('Typed worker messages require the popup surface.');
|
|
51
|
+
if (value.surface === 'page' && (value.verification.criteria_digest !== digest(value.verification.criteria) || Buffer.byteLength(JSON.stringify(value.verification.criteria)) > 4000)) throw failure('Page criteria identity changed or exceeds 4 KiB.');
|
|
40
52
|
return immutable(value);
|
|
41
53
|
}
|
|
54
|
+
export async function validateTaskDirectory(cwd, relative) {
|
|
55
|
+
const directory = path.resolve(cwd, safeRelative(relative));
|
|
56
|
+
if (path.relative(cwd, directory).split(path.sep).includes('..')) throw failure('Task directory must stay inside the workspace.');
|
|
57
|
+
let ancestor = directory;
|
|
58
|
+
for (;;) {
|
|
59
|
+
try {
|
|
60
|
+
if (await realpath(ancestor) !== ancestor || !(await stat(ancestor)).isDirectory()) throw failure('Task directory must use real directories inside the workspace.');
|
|
61
|
+
break;
|
|
62
|
+
} catch (error) { if (error.code !== 'ENOENT' || ancestor === cwd) throw error; ancestor = path.dirname(ancestor); }
|
|
63
|
+
}
|
|
64
|
+
if (path.relative(cwd, directory).split(path.sep).some(part => ['.git','.comal','.harness','.ssh','node_modules'].includes(part) || /^\.env(?:\.|$)/i.test(part))) throw failure('Protected task directory.');
|
|
65
|
+
return directory;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A fixture can predate the extension. Reading its identity must not require
|
|
69
|
+
// manifest.json or a complete publishable candidate at conversational entry.
|
|
70
|
+
export async function chromeFixtureDigest(cwd, task) {
|
|
71
|
+
const directory = await validateTaskDirectory(cwd, task.directory);
|
|
72
|
+
const file = path.join(directory, safeRelative(task.fixture.file));
|
|
73
|
+
const before = await lstat(file);
|
|
74
|
+
if (!before.isFile() || await realpath(file) !== file || before.size > 10000000) throw failure('The immutable fixture must be a bounded regular file without symlink aliases.', 'CHROME_TASK_FIXTURE_CHANGED');
|
|
75
|
+
const bytes = await readFile(file), after = await lstat(file);
|
|
76
|
+
if (!after.isFile() || await realpath(file) !== file || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.length !== before.size) throw failure('The immutable fixture changed while reading.', 'CHROME_TASK_FIXTURE_CHANGED');
|
|
77
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
78
|
+
}
|
|
42
79
|
|
|
43
80
|
// Host-owned coordinator. No executor or trust-management API is exposed to plugins.
|
|
44
|
-
export async function prepareChromeTask({ cwd, house, store, sessionId, executor, policy, task, source = 'explicit_cli_file', observeIdentity = true }) {
|
|
81
|
+
export async function prepareChromeTask({ cwd, house, store, sessionId, executor, policy, task, source = 'explicit_cli_file', provenance, observeIdentity = true, sourceEditFacts = false, verifier, publicationRecoveryGuard, checkpointProjection }) {
|
|
45
82
|
const loaded = await store.load(sessionId);
|
|
46
83
|
if (!loaded.journalAudit.consistent) throw failure('Invalid session journal.', 'AUDIT_INVALID');
|
|
47
84
|
const saved = savedChromeTask(loaded);
|
|
48
85
|
if (!task && !saved) return null;
|
|
49
86
|
const brief = normalizeTask(task || saved.task), taskDigest = digest(brief);
|
|
87
|
+
if (brief.surface === 'page') return (await import('./chrome-page-task.mjs')).prepareChromePageTask({ cwd, house, store, sessionId, executor, policy, task: brief, source, provenance, verifier, publicationRecoveryGuard, checkpointProjection });
|
|
50
88
|
const checkOperation = brief.tests ? 'ChromeTest' : 'ChromeVerify';
|
|
51
89
|
if (saved && saved.task_digest !== taskDigest) throw failure('El encargo guardado es inmutable; usa una sesión nueva para otro criterio.');
|
|
52
|
-
const directory =
|
|
53
|
-
if (await realpath(directory) !== directory || path.relative(cwd, directory).split(path.sep).includes('..')) throw failure('Task directory must be a real directory inside the workspace.');
|
|
90
|
+
const directory = await validateTaskDirectory(cwd, brief.directory);
|
|
54
91
|
const catalogue = ['Deploy','Deployment','Open','Targets','Assert','Contexts', ...(brief.tests ? ['Interact'] : [])].map(name => prefix+name);
|
|
55
92
|
if (catalogue.some(name => !house.registry.get(name))) throw failure('chrome-lab no está activo. Usa npm run lab:chrome -- --chrome-task ARCHIVO.json.');
|
|
56
93
|
const mechanisms = ['runtime:chrome-task.scope','runtime:chrome-task.close','runtime:chrome-task.artifact-hash'];
|
|
57
94
|
const compile = async version => compileGovernance(JSON.parse(await readFile(new URL(`../../governance/chrome-task-v${version}.json`, import.meta.url), 'utf8')), mechanisms);
|
|
58
|
-
const currentPlan = await compile(
|
|
59
|
-
const
|
|
60
|
-
const plan =
|
|
95
|
+
const currentPlan = await compile(brief.supplied_inputs?10:9), historical = saved ? await Promise.all([compile(1), compile(2), compile(3), compile(4), compile(5), compile(6), compile(7), compile(8), compile(9), compile(10)]) : [];
|
|
96
|
+
const plan = historical.find(plan => saved?.governance_plan_hash === plan.plan_hash) || currentPlan;
|
|
97
|
+
const legacy = plan.profile.version === '1', inspection = Number(plan.profile.version) >= 3, currency = Number(plan.profile.version) >= 4, pipeline = Number(plan.profile.version) >= 5, pages = Number(plan.profile.version) >= 6, documentProof=Number(plan.profile.version)>=7, workerInspection=Number(plan.profile.version)>=8, retainedSource=Number(plan.profile.version)>=9,suppliedInputs=Number(plan.profile.version)>=10;
|
|
98
|
+
if(brief.supplied_inputs&&!suppliedInputs)throw failure('Supplied inputs require their original v10 task policy.');
|
|
99
|
+
const suppliedDigest=brief.supplied_inputs?digest(brief.supplied_inputs):null;
|
|
100
|
+
if(suppliedDigest){
|
|
101
|
+
if(saved&&saved.supplied_inputs_digest!==suppliedDigest||provenance?.supplied_inputs_digest&&provenance.supplied_inputs_digest!==suppliedDigest)throw failure('The accepted supplied-input manifest changed.','CHROME_TASK_INPUT_CHANGED');
|
|
102
|
+
await verifySuppliedInputs(directory,brief.supplied_inputs);
|
|
103
|
+
}
|
|
104
|
+
let fixtureSource=null;
|
|
105
|
+
if(brief.fixture){
|
|
106
|
+
if(!pages||!house.registry.get(prefix+'Preview')||!house.registry.get(prefix+'CloseTab'))throw failure('Fixture coordination requires the current policy and Preview/CloseTab operations.');
|
|
107
|
+
fixtureSource=await chromeFixtureDigest(cwd,brief);
|
|
108
|
+
if(saved&&saved.fixture_source_digest!==fixtureSource || provenance?.fixture_source_digest&&provenance.fixture_source_digest!==fixtureSource)throw failure('The immutable test fixture changed.','CHROME_TASK_FIXTURE_CHANGED');
|
|
109
|
+
}
|
|
110
|
+
let baselineSource = null;
|
|
111
|
+
if(brief.baseline){
|
|
112
|
+
if(!pipeline)throw failure('Initial baseline coordination requires the current task policy.');
|
|
113
|
+
baselineSource=saved?.baseline_source_digest || provenance?.baseline_source_digest || manifestHash(await snapshotProject(cwd,brief.directory));
|
|
114
|
+
if(!/^[a-f0-9]{64}$/.test(baselineSource)||!saved&&manifestHash(await snapshotProject(cwd,brief.directory))!==baselineSource)throw failure('The original baseline candidate changed before adoption.','CHROME_TASK_STALE_SOURCE');
|
|
115
|
+
}
|
|
116
|
+
if (brief.tests?.some(step=>step.action==='message') && (!pipeline || !house.registry.get(prefix+'Message'))) throw failure('Typed worker messaging requires the current Chrome task policy and Message operation.');
|
|
117
|
+
if (inspection && ['ReadDOM', 'Console'].some(name => !house.registry.get(prefix + name))) throw failure('Chrome Lab DOM/console observations are unavailable.');
|
|
118
|
+
if(workerInspection&&!house.registry.get(prefix+'WorkerDiagnostics'))throw failure('Scoped native worker diagnostics are unavailable.');
|
|
61
119
|
if (saved && saved.governance_plan_hash !== plan.plan_hash) throw failure('The accepted workflow policy changed; it cannot be reactivated silently.');
|
|
62
|
-
//
|
|
120
|
+
// Existing task semantics retain their original policy; supplied inputs explicitly opt into v10. Historical approvals keep their original policy.
|
|
121
|
+
// Historical v1 approvals retain native
|
|
63
122
|
// text Read and never gain FileInfo access just by resuming on a newer client.
|
|
64
123
|
const artifactOperation = legacy ? 'Read' : 'FileInfo';
|
|
65
124
|
const hashOperation = house.registry.get('FileInfo');
|
|
@@ -84,73 +143,188 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
84
143
|
const checkpoint = session.rows.findLast(row => row.type === 'chrome_task_checkpoint' && row.task_digest === taskDigest);
|
|
85
144
|
return { ...(checkpoint?.state || { stage: 'new', status: 'ready' }) };
|
|
86
145
|
};
|
|
87
|
-
const snapshot = signal => snapshotProject(cwd, brief.directory, { signal });
|
|
146
|
+
const snapshot = async signal => {if(suppliedDigest)await verifySuppliedInputs(directory,brief.supplied_inputs,{signal});const result=await snapshotProject(cwd, brief.directory, { signal }); if(fixtureSource&&result.files.find(f=>f.path===brief.fixture.file)?.sha256!==fixtureSource)throw failure('The immutable test fixture changed.','CHROME_TASK_FIXTURE_CHANGED');if(suppliedDigest)assertSuppliedSnapshot(result,brief.supplied_inputs); return result;};
|
|
88
147
|
const save = (options, value) => options.recordWork({ type: 'chrome_task_checkpoint', source: 'workflow_host', task_digest: taskDigest, state: value });
|
|
89
148
|
const invoke = async (options, name, input) => JSON.parse(await options.invoke(prefix+name, input, { signal: options.signal }));
|
|
90
149
|
const assertReady = value => { if (value.stage.endsWith('_pending') || value.status === 'uncertain') throw failure('Publicación incierta: no se repite. Requiere reconciliación explícita.', 'CHROME_TASK_UNCERTAIN'); };
|
|
91
|
-
const
|
|
150
|
+
const observeDocument = async (options,binding,previousIdentity) => {
|
|
151
|
+
if(!observeIdentity)return;
|
|
152
|
+
const started=Date.now();let observed;
|
|
153
|
+
for(;;){
|
|
154
|
+
try{observed=await invoke(options,'Contexts',{target_id:binding.target_id});}catch(error){let detail;try{detail=JSON.parse(error.message);}catch{}if(detail?.code!=='CHROME_TARGET_NAVIGATING')throw error;}
|
|
155
|
+
const identity=observed?.identity;
|
|
156
|
+
if(identity?.context_unique_id&&identity?.connection_id&&(!previousIdentity||identity.context_unique_id!==previousIdentity.context_unique_id)){
|
|
157
|
+
if(previousIdentity&&(identity.connection_id!==previousIdentity.connection_id||identity.browser_endpoint_id!==previousIdentity.browser_endpoint_id))throw failure('The connection changed during document renewal.','CHROME_TASK_RELOAD_UNCERTAIN');
|
|
158
|
+
binding.identity=identity;return;
|
|
159
|
+
}
|
|
160
|
+
if(Date.now()-started>=3000)throw failure(previousIdentity?'Reload was dispatched but a new document was not observed; do not replay it.':'Document/connection identity unavailable.',previousIdentity?'CHROME_TASK_RELOAD_UNCERTAIN':'CHROME_TASK_UNCERTAIN');
|
|
161
|
+
await delay(100,undefined,{signal:options.signal});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const fixtureBinding = async (options,value,previousIdentity) => {
|
|
165
|
+
if(!brief.fixture)return;
|
|
166
|
+
const bound=value.fixture_binding;if(!bound)throw failure('No recorded fixture target. Publish the complete candidate.','CHROME_TASK_FIXTURE_MISSING');
|
|
167
|
+
const started=Date.now();for(;;){const live=(await invoke(options,'Targets',{})).targets,target=live.find(t=>t.target_id===bound.target_id);
|
|
168
|
+
if(target?.type==='page'&&target.url===bound.url)break;
|
|
169
|
+
if(!previousIdentity||target?.url&&target.url!=='about:blank'||Date.now()-started>=3000)throw failure('The exact owned fixture target changed.','CHROME_TASK_TARGET_CHANGED');
|
|
170
|
+
await delay(100,undefined,{signal:options.signal});
|
|
171
|
+
}
|
|
172
|
+
await observeDocument(options,bound,previousIdentity);await save(options,value);
|
|
173
|
+
};
|
|
174
|
+
const publishFixture = async (options,value,old) => {
|
|
175
|
+
if(!brief.fixture)return;
|
|
176
|
+
if(old.fixture_binding){
|
|
177
|
+
value.fixture_binding=old.fixture_binding;await save(options,value);
|
|
178
|
+
const found=(await invoke(options,'Targets',{})).targets.find(t=>t.target_id===old.fixture_binding.target_id);
|
|
179
|
+
if(found){
|
|
180
|
+
if(found.url!==old.fixture_binding.url)throw failure('Fixture navigation changed; cannot refresh a different resource.','CHROME_TASK_TARGET_CHANGED');
|
|
181
|
+
await fixtureBinding(options,value);const previousIdentity=documentProof?value.fixture_binding.identity:undefined;value.stage='fixture_reload_pending';await save(options,value);
|
|
182
|
+
await invoke(options,'Interact',{target_id:found.target_id,action:'reload'});
|
|
183
|
+
await fixtureBinding(options,value,previousIdentity);value.fixture_document_transition={before:previousIdentity,after:value.fixture_binding.identity};await save(options,value);return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
value.stage='fixture_open_pending';await save(options,value);const opened=await invoke(options,'Preview',{project:brief.project,file:brief.fixture.file});const url=new URL(opened.url);
|
|
187
|
+
if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||decodeURIComponent(url.pathname)!=='/'+brief.fixture.file)throw failure('Preview returned a different fixture resource.','CHROME_TASK_TARGET_CHANGED');
|
|
188
|
+
value.fixture_binding={target_id:opened.target_id,url:opened.url,project:brief.project,fixture_source_digest:fixtureSource};await save(options,value);await fixtureBinding(options,value);
|
|
189
|
+
};
|
|
190
|
+
const verify = async (options, runTests = false, rerun = false) => {
|
|
92
191
|
const value = await state(); assertReady(value);
|
|
93
192
|
if (!value.deployment) throw failure('Publica primero con ChromePublish; no hay una publicación registrada.');
|
|
94
193
|
if (manifestHash(await snapshot(options.signal)) !== value.source_digest) throw failure('Los archivos cambiaron desde la publicación. Usa ChromePublish antes de verificar.', 'CHROME_TASK_STALE_SOURCE');
|
|
95
194
|
const url = `chrome-extension://${value.deployment.extension_id}/${brief.popup}`;
|
|
96
|
-
const resolve = async
|
|
97
|
-
|
|
98
|
-
if (targets.length !== 1) throw failure('Popup propio ausente o ambiguo; no se abre ni elige otra pestaña.');
|
|
195
|
+
const resolve = async previousIdentity => {
|
|
196
|
+
let targets;const started=Date.now();for(;;){targets=(await invoke(options,'Targets',{})).targets.filter(t=>t.project===brief.project&&t.type==='page'&&t.url===url);if(targets.length===1)break;if(targets.length>1||!previousIdentity||Date.now()-started>=3000)throw failure('Popup propio ausente o ambiguo; no se abre ni elige otra pestaña.');await delay(100,undefined,{signal:options.signal});}
|
|
99
197
|
value.binding = { target_id: targets[0].target_id, project: brief.project, url };
|
|
100
198
|
await save(options, value);
|
|
101
|
-
|
|
102
|
-
let observed;
|
|
103
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
104
|
-
observed = await invoke(options, 'Contexts', { target_id: value.binding.target_id });
|
|
105
|
-
if (observed.identity?.context_unique_id && observed.identity?.connection_id) break;
|
|
106
|
-
await delay(100, undefined, { signal: options.signal });
|
|
107
|
-
}
|
|
108
|
-
if (!observed.identity?.context_unique_id || !observed.identity?.connection_id) throw failure('Document/connection identity unavailable.');
|
|
109
|
-
value.binding.identity = observed.identity; await save(options, value);
|
|
110
|
-
}
|
|
199
|
+
await observeDocument(options,value.binding,previousIdentity);await save(options,value);
|
|
111
200
|
};
|
|
112
201
|
await resolve();
|
|
202
|
+
await fixtureBinding(options,value);
|
|
203
|
+
const initialBindings=value.fixture_binding?structuredClone({popup:value.binding,page:value.fixture_binding}):null;
|
|
204
|
+
let publicationObservation;
|
|
205
|
+
if (currency && runTests) {
|
|
206
|
+
const observed = await invoke(options, 'Deployment', { project: brief.project, reconnect: false });
|
|
207
|
+
if (observed.status !== 'observed' || observed.project !== brief.project || observed.attempt_id !== value.attempt_id || observed.file_manifest_sha256 !== value.file_manifest_sha256 || observed.extension_id !== value.deployment.extension_id) throw failure('The physical publication no longer matches this candidate. Publish the accepted source before testing; no interaction executed.', 'CHROME_TASK_REMOTE_CHANGED');
|
|
208
|
+
publicationObservation = (await store.load(sessionId)).rows.findLast(row => row.type === 'house_receipt' && row.parent_invocation_id === options.invocation_id && row.operation === prefix + 'Deployment' && row.status === 'returned')?.invocation_id;
|
|
209
|
+
}
|
|
210
|
+
let evidenceReuse;
|
|
211
|
+
if (currency && runTests && observeIdentity && value.behavior_invocation_id) {
|
|
212
|
+
const book = await store.load(sessionId);
|
|
213
|
+
const receipt = book.rows.findLast(row => row.type === 'house_receipt' && row.invocation_id === value.behavior_invocation_id && row.operation === 'ChromeTest');
|
|
214
|
+
if (!receipt || receipt.status !== 'returned') throw failure('The previous behavior execution has no successful durable receipt; uncertain tests cannot be repeated or credited.', 'CHROME_TASK_UNCERTAIN');
|
|
215
|
+
const previous = JSON.parse(receipt.output);
|
|
216
|
+
const lastEffect = brief.tests.findLastIndex(step => step.action !== 'assert');
|
|
217
|
+
if (!rerun && lastEffect < brief.tests.length - 1 && receipt.governance_plan_hash === plan.plan_hash && receipt.output_digest === digest(receipt.output) && previous.status === 'verified' && previous.scope === 'declared_behavior' && !previous.evidence_reuse && previous.task_digest === taskDigest && previous.source_digest === value.source_digest && digest(previous.binding) === digest(value.binding) && (!brief.fixture || digest(previous.bindings)===digest(initialBindings)) && previous.tests?.length === brief.tests.length && previous.tests.every((test,index) => test.index === index && (brief.tests[index].action === 'assert' ? test.passed === true : test.executed === true && (!documentProof || brief.tests[index].action!=='reload' || test.document_transition?.before?.context_unique_id && test.document_transition.after?.context_unique_id!==test.document_transition.before.context_unique_id && test.document_transition.after?.connection_id===test.document_transition.before.connection_id)))) {
|
|
218
|
+
evidenceReuse = { behavior_receipt_id: receipt.invocation_id, scope: 'same_source_and_document_with_fresh_terminal_postconditions', last_effect_index: lastEffect };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
113
221
|
let result;
|
|
114
222
|
try { result = await invoke(options, 'Assert', { target_id: value.binding.target_id, selector: brief.selector, equals: brief.equals, timeout_ms: 0 }); }
|
|
115
223
|
catch (error) { let missing = false; try { missing = JSON.parse(error.message).code === 'CHROME_TARGET_MISSING'; } catch {} if (!missing) throw error; await resolve(); result = await invoke(options, 'Assert', { target_id: value.binding.target_id, selector: brief.selector, equals: brief.equals, timeout_ms: 0 }); }
|
|
224
|
+
let fixtureResult,failureSurface=result.passed?null:'popup';
|
|
225
|
+
if(brief.fixture){fixtureResult=await invoke(options,'Assert',{target_id:value.fixture_binding.target_id,selector:brief.fixture.selector,equals:brief.fixture.equals,timeout_ms:1000});if(!fixtureResult.passed){result.passed=false;failureSurface='page';}}
|
|
116
226
|
const tests = [];
|
|
227
|
+
if (currency && runTests && result.passed && !evidenceReuse) { value.behavior_invocation_id = options.invocation_id; await save(options, value); }
|
|
117
228
|
if (runTests && result.passed) for (const [index, step] of brief.tests.entries()) {
|
|
229
|
+
if (evidenceReuse && index <= evidenceReuse.last_effect_index) continue;
|
|
230
|
+
const targetBinding=step.surface==='page'?value.fixture_binding:value.binding;
|
|
118
231
|
if (step.action === 'assert') {
|
|
119
|
-
const observed = await invoke(options, 'Assert', { target_id:
|
|
232
|
+
const observed = await invoke(options, 'Assert', { target_id: targetBinding.target_id, selector: step.selector, ...(step.equals !== undefined ? { equals: step.equals } : {}), ...(step.count !== undefined ? { count: step.count } : {}), timeout_ms: 1000 });
|
|
120
233
|
tests.push({ index, passed: observed.passed, observed: observed.observed });
|
|
121
|
-
if (!observed.passed) { result.passed = false; break; }
|
|
234
|
+
if (!observed.passed) { result.passed = false;failureSurface=step.surface||'popup'; break; }
|
|
235
|
+
} else if (step.action === 'message') {
|
|
236
|
+
value.stage='test_pending';await save(options,value);
|
|
237
|
+
const observed=await invoke(options,'Message',{target_id:value.binding.target_id,request:step.request});
|
|
238
|
+
value.stage='testing';await save(options,value);
|
|
239
|
+
const passed=observed.received===true&&observed.url===url&&observed.response===step.equals;
|
|
240
|
+
tests.push({index,executed:true,passed,observed});if(!passed){result.passed=false;failureSurface='popup';break;}
|
|
122
241
|
} else {
|
|
242
|
+
const previousIdentity=step.action==='reload'&&documentProof?targetBinding.identity:undefined;
|
|
123
243
|
value.stage = 'test_pending'; await save(options, value);
|
|
124
|
-
await invoke(options, 'Interact', { target_id:
|
|
244
|
+
const interaction=await invoke(options, 'Interact', { target_id: targetBinding.target_id, action: step.action, ...(step.selector ? { selector: step.selector } : {}), ...(step.action === 'fill' ? { text: step.text } : {}) });
|
|
245
|
+
if(interaction.dispatched===false&&interaction.blocked===true&&interaction.code==='CHROME_INPUT_BLOCKED'&&interaction.target_id===targetBinding.target_id&&interaction.action===step.action&&interaction.selector===step.selector){
|
|
246
|
+
value.stage='testing';await save(options,value);tests.push({index,executed:false,passed:false,blocked_input:interaction});result.passed=false;failureSurface=step.surface||'popup';break;
|
|
247
|
+
}
|
|
248
|
+
if(interaction.dispatched!==true)throw failure('Native interaction dispatch is unknown; do not replay it.','CHROME_TASK_INPUT_UNCERTAIN');
|
|
249
|
+
if (step.action === 'reload') {if(step.surface==='page')await fixtureBinding(options,value,previousIdentity);else await resolve(previousIdentity);}
|
|
125
250
|
value.stage = 'testing'; await save(options, value);
|
|
126
|
-
|
|
127
|
-
tests.push({ index, executed: true });
|
|
251
|
+
tests.push({ index, executed: true,...(previousIdentity?{document_transition:{before:previousIdentity,after:(step.surface==='page'?value.fixture_binding:value.binding).identity}}:{}) });
|
|
128
252
|
}
|
|
129
253
|
}
|
|
130
254
|
if (observeIdentity) {
|
|
131
255
|
const after = await invoke(options, 'Contexts', { target_id: value.binding.target_id });
|
|
132
256
|
if (digest(after.identity) !== digest(value.binding.identity)) throw failure('Document/connection changed during verification.');
|
|
257
|
+
if(value.fixture_binding){const afterPage=await invoke(options,'Contexts',{target_id:value.fixture_binding.target_id});if(digest(afterPage.identity)!==digest(value.fixture_binding.identity))throw failure('Fixture document/connection changed during verification.');}
|
|
133
258
|
}
|
|
134
259
|
if (result.observed?.url !== url || manifestHash(await snapshot(options.signal)) !== value.source_digest) throw failure('Evidence no longer matches source/resource.');
|
|
135
|
-
value.status = result.passed ? 'verified' : 'needs_correction'; value.stage = 'checked'; value.verified_scope = runTests ? 'declared_behavior' : 'declared_dom';
|
|
260
|
+
value.failure_surface=result.passed?null:failureSurface;value.status = result.passed ? 'verified' : 'needs_correction'; value.stage = 'checked'; value.verified_scope = runTests ? 'declared_behavior' : 'declared_dom';
|
|
261
|
+
if(baselineSource&&!value.baseline_observed&&value.source_digest===baselineSource)value.baseline_observed={source_digest:baselineSource,observed:result.observed,passed:result.passed};
|
|
136
262
|
await save(options, value);
|
|
137
|
-
return { status: value.status, scope: value.verified_scope, task_digest: taskDigest, source_digest: value.source_digest, binding: value.binding, observed: result.observed, ...(runTests ? { tests } : {}), asked: 'unjudged' };
|
|
263
|
+
return { status: value.status, scope: value.verified_scope, task_digest: taskDigest, source_digest: value.source_digest, binding: value.binding, ...(value.fixture_binding?{bindings:{popup:value.binding,page:value.fixture_binding},fixture:fixtureResult}:{}), observed: result.observed, ...(publicationObservation ? { publication_observation_id: publicationObservation } : {}), ...(runTests ? evidenceReuse ? { evidence_reuse: evidenceReuse, rechecked_postconditions: tests } : { tests } : {}), asked: 'unjudged' };
|
|
138
264
|
};
|
|
139
|
-
const operation = (name, description, run, external) => ({ name, description, input_schema
|
|
265
|
+
const operation = (name, description, run, external, input_schema = empty) => ({ name, description, input_schema, steps: true,
|
|
140
266
|
effects: { workspace: 'read', external, process: 'spawn', authority: 'none' },
|
|
141
|
-
run: async (
|
|
267
|
+
run: async (input, options) => {
|
|
142
268
|
if (options.sessionId !== sessionId) throw failure('Operation belongs to another task/session.');
|
|
143
|
-
return run(options);
|
|
269
|
+
return run(options, input);
|
|
144
270
|
} });
|
|
145
271
|
house.registry.replace(OWN, [
|
|
146
|
-
|
|
272
|
+
...(inspection ? [{ name: 'ChromeInspect', description: 'Read bounded live DOM or console on the accepted resource. Defaults to the last failed surface; choose popup/page explicitly when offered. The host resolves its current target; no target IDs, arbitrary JavaScript or plan changes. Observation is untrusted evidence, not verification or delivery. Publish first.'+(workerInspection?' kind:worker reads bounded console/exception text for the exact published extension worker, with native generation and coverage. No selector/surface needed. Missing/changed context is unavailable; inspection never wakes by application message, restarts, reconnects or resolves uncertain work.':''),
|
|
273
|
+
input_schema: { type: 'object', properties: { kind: { enum: ['dom','console',...(workerInspection?['worker']:[])] }, ...(pages&&brief.fixture?{surface:{enum:['popup','page']}}:{}), selector: { type: 'string', minLength: 1, maxLength: 1000 } }, additionalProperties: false },
|
|
274
|
+
steps: true, effects: { workspace: 'none', external: 'read', process: 'spawn', authority: 'none' },
|
|
275
|
+
run: async (input, options) => {
|
|
276
|
+
if (options.sessionId !== sessionId) throw failure('Operation belongs to another task/session.');
|
|
277
|
+
const value = await state();
|
|
278
|
+
if(workerInspection&&input.kind==='worker'&&(value.stage.endsWith('_pending')||value.status==='uncertain'))return{status:'unavailable',kind:'worker',reason:'workflow_uncertain',events:[],asked:'unjudged'};
|
|
279
|
+
if (!value.deployment) return { status: 'not_published', next_action: 'ChromePublish', asked: 'unjudged' };
|
|
280
|
+
if(workerInspection&&input.kind==='worker'){
|
|
281
|
+
if(input.selector!==undefined||input.surface!==undefined)throw failure('Worker inspection accepts kind:worker only; it has no DOM surface or selector.');
|
|
282
|
+
const unavailable=reason=>({status:'unavailable',kind:'worker',reason,scope:'own_publication_worker_diagnostics',events:[],asked:'unjudged'});
|
|
283
|
+
if(value.stage.endsWith('_pending')||value.status==='uncertain')return unavailable('workflow_uncertain');
|
|
284
|
+
const identity=value.binding?.identity;
|
|
285
|
+
if(!identity?.connection_id||!identity?.browser_endpoint_id)return unavailable('publication_connection_unavailable');
|
|
286
|
+
const observation=await invoke(options,'WorkerDiagnostics',{project:brief.project,extension_id:value.deployment.extension_id,attempt_id:value.attempt_id,file_manifest_sha256:value.file_manifest_sha256,connection_id:identity.connection_id,browser_endpoint_id:identity.browser_endpoint_id,limit:30});
|
|
287
|
+
const after=await state();
|
|
288
|
+
if(after.attempt_id!==value.attempt_id||after.source_digest!==value.source_digest||after.stage!==value.stage||after.status!==value.status)return unavailable('workflow_changed');
|
|
289
|
+
return{status:observation.status,kind:'worker',scope:'own_publication_worker_diagnostics',publication_source_digest:value.source_digest,observation,asked:'unjudged'};
|
|
290
|
+
}
|
|
291
|
+
const url = `chrome-extension://${value.deployment.extension_id}/${brief.popup}`;
|
|
292
|
+
const surface=input.surface||value.failure_surface||'popup',expectedUrl=surface==='page'?value.fixture_binding?.url:url;
|
|
293
|
+
const targets = (await invoke(options, 'Targets', {})).targets.filter(t => t.type === 'page' && t.url === expectedUrl && (surface==='page'?t.target_id===value.fixture_binding?.target_id:t.project===brief.project));
|
|
294
|
+
if (targets.length !== 1) throw failure('Own popup absent or ambiguous; inspection cannot select another target.');
|
|
295
|
+
const binding = { target_id: targets[0].target_id, project: brief.project, url:expectedUrl };
|
|
296
|
+
value.inspection_binding = binding; await save(options, value);
|
|
297
|
+
const kind = input.kind || 'dom';
|
|
298
|
+
const observation = await invoke(options, kind === 'dom' ? 'ReadDOM' : 'Console', { target_id: binding.target_id, ...(kind === 'dom' ? { selector: input.selector || 'body' } : { limit: 100 }) });
|
|
299
|
+
if (kind === 'dom' && observation.value?.url !== expectedUrl || !(await invoke(options, 'Targets', {})).targets.some(t => t.target_id === binding.target_id && t.url === expectedUrl && (surface==='page'||t.project === brief.project))) throw failure('The own popup changed during inspection.', 'CHROME_TASK_TARGET_CHANGED');
|
|
300
|
+
return { status: 'observed', kind, scope: 'own_published_popup_observation', publication_source_digest: value.source_digest, binding, observation, asked: 'unjudged' };
|
|
301
|
+
}
|
|
302
|
+
}] : []),
|
|
303
|
+
operation('ChromePublish', 'Publish the accepted source and observe its criterion. The host manages identity and versions. Identical bytes reuse only a physically matching publication. Failed criteria include approved DOM/console diagnostics. Edit source then publish; never retries an uncertain mutation. Children retain permissions.', async options => {
|
|
147
304
|
const old = await state(); assertReady(old);
|
|
148
|
-
|
|
305
|
+
let before;
|
|
306
|
+
try { before=await snapshot(options.signal); }
|
|
307
|
+
catch(error) { if(error.code==='CHROME_TASK_SOURCE_INVALID')await options.recordWork({type:'chrome_candidate_rejected',source:'workflow_host',stage:'source_preflight',task_digest:taskDigest,code:error.code,remote_dispatched:false,details:error.details});throw error; }
|
|
149
308
|
const continuing = old.stage === 'reconciled' || old.recovery_pending && manifestHash(before) === old.source_digest;
|
|
309
|
+
const diagnostics = async result => {
|
|
310
|
+
const allowed = name => policy.allowed.has(prefix + name) && !policy.denied.has(prefix + name);
|
|
311
|
+
if (inspection && result.status === 'needs_correction' && allowed('ReadDOM') && allowed('Console')) {
|
|
312
|
+
const dom = JSON.parse(await options.invoke('ChromeInspect', { kind: 'dom', selector: 'body' }, { signal: options.signal }));
|
|
313
|
+
const console = JSON.parse(await options.invoke('ChromeInspect', { kind: 'console' }, { signal: options.signal }));
|
|
314
|
+
const page=brief.fixture?JSON.parse(await options.invoke('ChromeInspect',{kind:'dom',surface:'page',selector:'body'},{signal:options.signal})):null;
|
|
315
|
+
return { ...result, diagnostics: { scope: 'untrusted_own_resource_observations', dom: dom.observation, console: console.observation,...(page?{page:page.observation}:{}) } };
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
};
|
|
319
|
+
if (inspection && !continuing && old.deployment && old.source_digest === manifestHash(before) && ['published', 'checked'].includes(old.stage)) {
|
|
320
|
+
const observed = await invoke(options, 'Deployment', { project: brief.project, reconnect: false });
|
|
321
|
+
if (observed.status === 'observed' && observed.attempt_id === old.attempt_id && observed.file_manifest_sha256 === old.file_manifest_sha256 && observed.extension_id === old.deployment.extension_id) return diagnostics({ ...await verify(options), reused_publication: true });
|
|
322
|
+
}
|
|
150
323
|
if (continuing && manifestHash(before) !== old.source_digest) throw failure('El candidato reconciliado cambió; conserva su snapshot antes de continuar.', 'CHROME_TASK_STALE_SOURCE');
|
|
151
|
-
const
|
|
324
|
+
const namespace=pages?await invoke(options,'Status',{}):null;const namespaceReceipt=(await store.load(sessionId)).rows.findLast(r=>r.type==='house_receipt'&&r.parent_invocation_id===options.invocation_id&&r.operation===prefix+'Status'&&r.status==='returned');
|
|
325
|
+
const value = continuing ? { ...old } : { ...(old.baseline_observed?{baseline_observed:old.baseline_observed}:{}), stage: 'deploy_pending', status: 'ready', source_digest: manifestHash(before), file_manifest_sha256: createHash('sha256').update(JSON.stringify(before.files.map(({path,sha256})=>({path,sha256})))).digest('hex'), attempt_id: randomUUID(),publication_namespace:{create_only:Boolean(namespace&&!namespace.extensions_truncated&&!namespace.extensions.some(e=>e.project===brief.project)),existing:namespace?namespace.extensions.some(e=>e.project===brief.project):true,receipt_id:namespaceReceipt?.invocation_id} }; await save(options, value);
|
|
152
326
|
try {
|
|
153
|
-
if (!continuing) value.deployment = await invoke(options, 'Deploy', { project: brief.project, directory: brief.directory, attempt_id: value.attempt_id });
|
|
327
|
+
if (!continuing) value.deployment = await invoke(options, 'Deploy', { project: brief.project, directory: brief.directory, attempt_id: value.attempt_id, ...(value.publication_namespace?.create_only?{create_only:true}:{}),...(suppliedInputs?{expected_manifest_sha256:value.file_manifest_sha256}:{}) });
|
|
154
328
|
if (value.deployment.project !== brief.project) throw failure('Deployment returned another project.');
|
|
155
329
|
if (value.deployment.attempt_id !== value.attempt_id) throw failure('Deployment returned another attempt.');
|
|
156
330
|
if (!value.deployment.file_manifest_sha256 || value.deployment.file_manifest_sha256 !== createHash('sha256').update(JSON.stringify(before.files.map(({path,sha256})=>({path,sha256})))).digest('hex')) throw failure('Deployment snapshot differs from the accepted local candidate.');
|
|
@@ -158,14 +332,19 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
158
332
|
value.stage = 'open_pending'; await save(options, value);
|
|
159
333
|
await invoke(options, 'Open', { url: `chrome-extension://${value.deployment.extension_id}/${brief.popup}` });
|
|
160
334
|
}
|
|
335
|
+
await publishFixture(options,value,old);
|
|
161
336
|
value.stage = 'published'; await save(options, value);
|
|
162
337
|
} catch (error) {
|
|
163
|
-
|
|
338
|
+
if(suppliedInputs&&error.code==='CHROME_DEPLOY_PRECONDITION'){
|
|
339
|
+
value.status='ready';value.stage='source_rejected';await save(options,value);
|
|
340
|
+
await options.recordWork({type:'chrome_candidate_rejected',source:'workflow_host',stage:'deployment_snapshot_precondition',task_digest:taskDigest,code:error.code,remote_dispatched:false,details:error.details});throw error;
|
|
341
|
+
}
|
|
342
|
+
value.status = ['HOUSE_REFUSED','HOUSE_GUARD','GOVERNANCE_REFUSED','CHROME_PROJECT_EXISTS'].includes(error.code) ? 'refused' : 'uncertain';
|
|
164
343
|
if (value.status === 'refused') value.stage = 'refused'; await save(options, value); throw error;
|
|
165
344
|
}
|
|
166
345
|
const result=await verify(options);
|
|
167
346
|
if(continuing){const current=await state();delete current.recovery_pending;delete current.recovery_popup_observed;await save(options,current);}
|
|
168
|
-
return result;
|
|
347
|
+
return diagnostics(result);
|
|
169
348
|
}, 'write'),
|
|
170
349
|
operation('ChromeRecover', 'Explicit read-only recovery after a publication/connection interruption. Reconnects, matches the durable attempt and physical remote snapshot to the accepted local candidate. Never repeats Deploy/Open/Interact. A missing popup after confirmed Deploy returns recovered with next_action ChromePublish, which opens it without publishing again. Unknown interactions and ambiguous/missing evidence remain blocked.', async options => {
|
|
171
350
|
const old = await state();
|
|
@@ -184,8 +363,8 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
184
363
|
if (targets.length) return verify(options);
|
|
185
364
|
return { status: 'recovered', stage: value.stage, attempt_id: value.attempt_id, next_action: 'ChromePublish', note: 'Publicación observada; ChromePublish sólo abrirá el popup y verificará. No repite Deploy.' };
|
|
186
365
|
}, 'read'),
|
|
187
|
-
operation('ChromeVerify', 'Read the accepted own popup and check its fixed criterion against the currently published source and fresh document identity. No publication or navigation. Failed criterion means correct source and publish.', verify, 'read'),
|
|
188
|
-
...(brief.tests ? [operation('ChromeTest', 'Run the accepted interaction/assertion sequence on the own popup. This writes test data and may reload; each child retains permissions. No test steps or targets to supply. A partial/uncertain interaction stops the sequence.', options => verify(options, true), 'write')] : []),
|
|
366
|
+
operation('ChromeVerify', 'Read the accepted own popup and check its fixed criterion against the currently published source and fresh document identity. No publication or navigation. Failed criterion means correct source and publish.', options => verify(options), 'read'),
|
|
367
|
+
...(brief.tests ? [operation('ChromeTest', currency ? 'Verify accepted behavior. The house reuses a completed test receipt only for identical source/document identity and freshly passing final postconditions; otherwise runs the sequence. rerun:true explicitly repeats actions. Child permissions remain; uncertain tests never replay.' : 'Run the accepted interaction/assertion sequence on the own popup. This writes test data and may reload; each child retains permissions. No test steps or targets to supply. A partial/uncertain interaction stops the sequence.', (options, input) => verify(options, true, currency && input.rerun === true), 'write', currency ? { type: 'object', properties: { rerun: { type: 'boolean' } }, additionalProperties: false } : empty)] : []),
|
|
189
368
|
operation('ChromeFinish', 'Verify the declared browser check and close the accepted work with fresh evidence. No versions/IDs/criteria to supply. Returns done only after the host check; does not authorize publishing elsewhere.', async options => {
|
|
190
369
|
const current = workProjection(await store.load(sessionId));
|
|
191
370
|
if (current?.counts.done === 1) { const result = JSON.parse(await options.invoke(checkOperation, {}, { signal: options.signal })); if (result.status !== 'verified') throw failure('Current browser criterion failed.', 'WORK_UNVERIFIED'); return { status: 'done', reobserved: true, asked: 'unjudged' }; }
|
|
@@ -193,40 +372,80 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
193
372
|
await options.invoke('WorkStepRun', { task_id: 'chrome-task', plan_version: current.plan.version, step_id: 'browser', recheck: true }, { signal: options.signal });
|
|
194
373
|
await options.invoke('WorkTaskUpdate', { task_id: 'chrome-task', plan_version: current.plan.version, status: 'done' }, { signal: options.signal });
|
|
195
374
|
return { status: 'done', scope: 'declared_work_checks', asked: 'unjudged' };
|
|
196
|
-
}, 'read')
|
|
375
|
+
}, 'read'),
|
|
376
|
+
...(pipeline ? [operation('ChromeDeliver', 'Commit a complete source candidate: the house publishes, runs accepted tests and closes. Use after all source files are ready. Stops at an observed failure for you to correct; no plan, test, target or version arguments. Every child retains its permission and durable receipt. An uncertain effect is never replayed. done proves declared checks, not human acceptance.', async options => {
|
|
377
|
+
const publication = JSON.parse(await options.invoke('ChromePublish', {}, { signal: options.signal }));
|
|
378
|
+
if (publication.status !== 'verified') return { status: 'needs_correction', stage: 'primary_criterion', publication, next_action: 'Correct the observed source failure, then commit the complete candidate with ChromeDeliver.', asked: 'unjudged' };
|
|
379
|
+
if (brief.tests) {
|
|
380
|
+
const tests = JSON.parse(await options.invoke('ChromeTest', {}, { signal: options.signal }));
|
|
381
|
+
if (tests.status !== 'verified') {
|
|
382
|
+
const diagnostics={scope:'untrusted_own_resource_observations'};
|
|
383
|
+
if(inspection&&!policy.denied.has('ChromeInspect')&&['ReadDOM','Console'].every(name=>policy.allowed.has(prefix+name)&&!policy.denied.has(prefix+name))){
|
|
384
|
+
for(const [kind,key]of[['dom','dom'],['console','console']])try{diagnostics[key]=JSON.parse(await options.invoke('ChromeInspect',{kind,...(kind==='dom'?{selector:'body'}:{})},{signal:options.signal})).observation;}catch(error){if(options.signal?.aborted)throw error;diagnostics[key]={unavailable:true,code:error.code||'OBSERVATION_UNAVAILABLE'};}
|
|
385
|
+
}
|
|
386
|
+
return { status: 'needs_correction', stage: 'declared_behavior', check: tests, ...(Object.keys(diagnostics).length>1?{diagnostics}:{}), next_action: 'Correct the failing behavior using its observed diagnostics, then use ChromeDeliver. Do not change the accepted tests. A DOM marker is not proof of worker communication or persisted data.', asked: 'unjudged' };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await options.invoke('ChromeFinish', {}, { signal: options.signal });
|
|
390
|
+
const book = await store.load(sessionId), current = await state();
|
|
391
|
+
const receipt = book.rows.findLast(r => r.type === 'house_receipt' && r.operation === checkOperation && r.status === 'returned');
|
|
392
|
+
return { status: 'done', scope: brief.tests ? 'declared_behavior_and_source_snapshot' : 'declared_dom_criterion_and_source_snapshot', source_digest: current.source_digest, task_digest: taskDigest, receipt_id: receipt?.invocation_id, tests: brief.tests?.length || 0, asked: 'unjudged' };
|
|
393
|
+
}, 'write')] : [])
|
|
197
394
|
]);
|
|
198
395
|
registerWorkOperations({ store, house, artifactObservation: { sessionId, operation: artifactOperation } });
|
|
199
396
|
if (!saved) {
|
|
200
|
-
await store.append(sessionId, { type: 'chrome_task_accepted', source: 'workflow_host', acceptance: source, task: brief, task_digest: taskDigest, governance_plan_hash: plan.plan_hash, governance_profile: plan.profile, artifact_observation: { operation: artifactOperation, hash: true, paths: declaredTask.artifacts } });
|
|
201
|
-
await store.append(sessionId, { type: '
|
|
397
|
+
await store.append(sessionId, { type: 'chrome_task_accepted', source: 'workflow_host', acceptance: source, ...(provenance ? { provenance: structuredClone(provenance) } : {}), ...(baselineSource?{baseline_source_digest:baselineSource}:{}), ...(fixtureSource?{fixture_source_digest:fixtureSource}:{}),...(suppliedDigest?{supplied_inputs_digest:suppliedDigest}:{}), task: brief, task_digest: taskDigest, governance_plan_hash: plan.plan_hash, governance_profile: plan.profile, artifact_observation: { operation: artifactOperation, hash: true, paths: declaredTask.artifacts } });
|
|
398
|
+
await store.append(sessionId, { type: 'chrome_task_scope', source: 'workflow_host', text: `Accepted Chrome task: ${brief.project}, directory ${brief.directory}, popup ${brief.popup}, ${brief.selector} equals ${JSON.stringify(brief.equals)}. Publish, correct and verify only this own extension; no extra permissions.` });
|
|
202
399
|
}
|
|
400
|
+
let sourceAccess;
|
|
401
|
+
const sourcePath = async (file,mutable) => {
|
|
402
|
+
if(suppliedDigest&&mutable){if(isSuppliedPath(path.relative(directory,path.resolve(cwd,file)),brief.supplied_inputs))return {allowed:false,reason:'Supplied inputs are read-only; implement the application around the accepted assets.'};await verifySuppliedInputs(directory,brief.supplied_inputs);}
|
|
403
|
+
if(baselineSource&&mutable&&!(await state()).baseline_observed)return {allowed:false,reason:'The initial baseline must be observed before edits. The house performs it under current permissions; do not modify the accepted initial bytes.'};
|
|
404
|
+
if(fixtureSource&&mutable&&path.resolve(cwd,file)===path.join(directory,brief.fixture.file))return {allowed:false,reason:'The accepted test fixture is immutable; implement the extension, not its oracle.'};
|
|
405
|
+
const relative = path.relative(directory, path.resolve(cwd, file));
|
|
406
|
+
const lexical = Boolean(relative) && !path.isAbsolute(relative) && !relative.split(path.sep).some(part => part === '..' || ['.git','.comal','.harness','.ssh','node_modules'].includes(part) || /^\.env(?:\.|$)/i.test(part));
|
|
407
|
+
if (!lexical) return { allowed: false, reason: 'Only files of the accepted extension.' };
|
|
408
|
+
await validateTaskDirectory(cwd, brief.directory);
|
|
409
|
+
let ancestor = path.resolve(cwd, file);
|
|
410
|
+
for (;;) {
|
|
411
|
+
try { return { allowed: await realpath(ancestor) === ancestor, reason: 'Source paths cannot alias another resource through symlinks.' }; }
|
|
412
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; ancestor = path.dirname(ancestor); }
|
|
413
|
+
}
|
|
414
|
+
};
|
|
203
415
|
const scope = async view => {
|
|
204
416
|
if (policy.denied.has(view.operation)) return { allowed: false, reason: 'Explicit session deny.' };
|
|
205
417
|
if (view.operation === 'WorkPlanSet' || view.operation === 'WorkCheckRepair') return { allowed: false, reason: 'Accepted task/criterion is immutable in this workflow.' };
|
|
206
418
|
if (view.actor.kind === 'human' || view.surface === 'internal') return true;
|
|
207
|
-
if (['Read','Edit','Write'].includes(view.operation))
|
|
208
|
-
|
|
209
|
-
return { allowed: Boolean(relative) && !path.isAbsolute(relative) && !relative.split(path.sep).some(part => part === '..' || ['.git','.comal','.harness','.ssh','node_modules'].includes(part) || /^\.env(?:\.|$)/i.test(part)), reason: 'Only files of the accepted extension.' };
|
|
210
|
-
}
|
|
419
|
+
if (['Read','Edit','Write'].includes(view.operation)) return sourcePath(view.input.file_path,view.operation!=='Read');
|
|
420
|
+
if (retainedSource && CHROME_SOURCE_NAMES.includes(view.operation)) return Boolean(sourceAccess?.active() && await sourceAccess.admit(view));
|
|
211
421
|
if (!legacy && view.operation === 'FileInfo') return { allowed: true, reason: 'Subject to the separately enforced artifact-hash capability.' };
|
|
212
422
|
if (OWN.includes(view.operation)) return true;
|
|
213
423
|
if (INTERNAL.has(view.operation)) return { allowed: ['ChromeFinish','WorkTaskUpdate'].includes(view.parent_operation), reason: 'Work administration belongs to the coordinator.' };
|
|
214
424
|
if (view.operation.startsWith(prefix)) {
|
|
215
|
-
if (!['ChromePublish','ChromeVerify','ChromeTest','ChromeRecover'].includes(view.parent_operation)) return { allowed: false, reason: 'Use the offered domain action.' };
|
|
425
|
+
if (!['ChromePublish','ChromeVerify','ChromeTest','ChromeRecover', ...(inspection ? ['ChromeInspect'] : [])].includes(view.parent_operation)) return { allowed: false, reason: 'Use the offered domain action.' };
|
|
216
426
|
const value = await state();
|
|
217
|
-
if
|
|
218
|
-
|
|
427
|
+
if(view.operation===prefix+'WorkerDiagnostics'){
|
|
428
|
+
const identity=value.binding?.identity;
|
|
429
|
+
return workerInspection&&view.parent_operation==='ChromeInspect'&&!value.stage.endsWith('_pending')&&value.status!=='uncertain'&&!policy.denied.has(prefix+'Deployment')&&digest(view.input)===digest({project:brief.project,extension_id:value.deployment?.extension_id,attempt_id:value.attempt_id,file_manifest_sha256:value.file_manifest_sha256,connection_id:identity?.connection_id,browser_endpoint_id:identity?.browser_endpoint_id,limit:30});
|
|
430
|
+
}
|
|
431
|
+
if (view.operation === prefix+'Deployment') return (view.parent_operation === 'ChromeRecover' || inspection && view.parent_operation === 'ChromePublish' || currency && view.parent_operation === 'ChromeTest' && view.input.reconnect === false) && view.input.project === brief.project;
|
|
432
|
+
if (view.operation === prefix+'Deploy') return (!suppliedInputs||view.input.expected_manifest_sha256===value.file_manifest_sha256)&&view.parent_operation === 'ChromePublish' && view.input.project === brief.project && view.input.directory === brief.directory && view.input.attempt_id === value.attempt_id && Boolean(view.input.create_only)===Boolean(value.publication_namespace?.create_only);
|
|
219
433
|
if (view.operation === prefix+'Open') return view.parent_operation === 'ChromePublish' && view.input.url === `chrome-extension://${value.deployment?.extension_id}/${brief.popup}`;
|
|
434
|
+
if (pages&&brief.fixture&&view.operation===prefix+'Preview')return view.parent_operation==='ChromePublish'&&view.input.project===brief.project&&view.input.file===brief.fixture.file&&value.stage==='fixture_open_pending';
|
|
435
|
+
if (view.operation === prefix+'Status') return pages && view.parent_operation==='ChromePublish';
|
|
220
436
|
if (view.operation === prefix+'Targets') return true;
|
|
221
|
-
if (view.operation === prefix+
|
|
222
|
-
if
|
|
437
|
+
if (inspection && ['ReadDOM','Console'].some(name => view.operation === prefix+name)) return view.parent_operation === 'ChromeInspect' && view.input.target_id === value.inspection_binding?.target_id;
|
|
438
|
+
if(pages&&brief.fixture&&view.operation===prefix+'Interact'&&view.parent_operation==='ChromePublish')return value.stage==='fixture_reload_pending'&&view.input.target_id===value.fixture_binding?.target_id&&view.input.action==='reload'&&view.input.selector===undefined&&view.input.text===undefined;
|
|
439
|
+
if (view.operation === prefix+'Interact') return view.parent_operation === 'ChromeTest' && (view.input.target_id === value.binding?.target_id || pages&&view.input.target_id===value.fixture_binding?.target_id) && brief.tests.some(step => (step.surface==='page'?value.fixture_binding?.target_id:value.binding?.target_id)===view.input.target_id && step.action === view.input.action && step.selector === view.input.selector && (step.action !== 'fill' || step.text === view.input.text));
|
|
440
|
+
if (pipeline && view.operation === prefix+'Message') return view.parent_operation === 'ChromeTest' && view.input.target_id === value.binding?.target_id && brief.tests.some(step=>step.action==='message'&&step.request===view.input.request);
|
|
441
|
+
if (['Contexts','Assert'].some(name => view.operation === prefix+name)) return (view.input.target_id === value.binding?.target_id || pages&&view.input.target_id===value.fixture_binding?.target_id) && (view.operation !== prefix+'Assert' || view.input.target_id===value.fixture_binding?.target_id && view.input.selector===brief.fixture?.selector && view.input.equals===brief.fixture?.equals || view.input.selector === brief.selector && view.input.equals === brief.equals || view.parent_operation === 'ChromeTest' && brief.tests.some(step => (step.surface==='page'?value.fixture_binding?.target_id:value.binding?.target_id)===view.input.target_id && step.action === 'assert' && step.selector === view.input.selector && step.equals === view.input.equals && step.count === view.input.count));
|
|
223
442
|
}
|
|
224
443
|
return { allowed: false, reason: 'Operation outside this accepted workflow surface.' };
|
|
225
444
|
};
|
|
226
|
-
const artifactHash = view => ({
|
|
445
|
+
const artifactHash = async view => ({
|
|
227
446
|
allowed: !policy.denied.has(view.operation) && view.contract_digest === hashOperation?.digest &&
|
|
228
|
-
['WorkStepRun','WorkTaskUpdate'].includes(view.parent_operation) &&
|
|
229
|
-
digest(view.input) === digest({ file_path: declaredTask.artifacts[0], hash: true }),
|
|
447
|
+
(['WorkStepRun','WorkTaskUpdate'].includes(view.parent_operation) &&
|
|
448
|
+
digest(view.input) === digest({ file_path: declaredTask.artifacts[0], hash: true }) || retainedSource && await sourceAccess?.inspection(view)),
|
|
230
449
|
reason: 'Only native full-file hashing of the exact accepted artifact by the host work coordinator.'
|
|
231
450
|
});
|
|
232
451
|
const close = async view => {
|
|
@@ -239,21 +458,80 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
239
458
|
if (!workProjection(await store.load(sessionId))) await executor.execute('WorkPlanSet', { objective: `Verify accepted Chrome task ${brief.project} (${taskDigest})`, expected_version: 0, tasks: [declaredTask] });
|
|
240
459
|
const initial = workProjection(await store.load(sessionId));
|
|
241
460
|
if (initial.current?.status === 'pending') await executor.execute('WorkTaskUpdate', { task_id: 'chrome-task', plan_version: initial.plan.version, status: 'in_progress' });
|
|
461
|
+
if(retainedSource)sourceAccess=bindChromeSource({executor,store,sessionId,cwd,plan,taskDigest,pathAllowed:async(file,mutable)=>(await sourcePath(file,mutable)).allowed,directory:brief.directory});
|
|
242
462
|
await house.activateGovernance({ sessionId, plan, bindings: {
|
|
243
|
-
'runtime:chrome-task.scope': { revision: legacy ? 'chrome-scope-v3' : 'chrome-scope-
|
|
463
|
+
'runtime:chrome-task.scope': { revision: suppliedInputs?'chrome-scope-v15':retainedSource?'chrome-scope-v14':workerInspection?'chrome-scope-v13':legacy ? 'chrome-scope-v3' : documentProof ? 'chrome-scope-v12' : pages ? 'chrome-scope-v11' : pipeline ? 'chrome-scope-v9' : currency ? 'chrome-scope-v7' : inspection ? 'chrome-scope-v6' : 'chrome-scope-v5', operations: ['*'], configuration_digest: taskDigest, check: scope },
|
|
244
464
|
'runtime:chrome-task.close': { revision: 'chrome-close-v1', operations: ['WorkTaskUpdate'], configuration_digest: taskDigest, check: close },
|
|
245
|
-
...(!legacy ? { 'runtime:chrome-task.artifact-hash': { revision: 'chrome-artifact-hash-v1', operations: ['FileInfo'], configuration_digest: digest({ taskDigest, artifacts: declaredTask.artifacts, contract_digest: hashOperation.digest }), check: artifactHash } } : {})
|
|
465
|
+
...(!legacy ? { 'runtime:chrome-task.artifact-hash': { revision: retainedSource?'chrome-artifact-hash-v2':'chrome-artifact-hash-v1', operations: ['FileInfo'], configuration_digest: digest({ taskDigest, artifacts: declaredTask.artifacts, contract_digest: hashOperation.digest }), check: artifactHash } } : {})
|
|
246
466
|
} });
|
|
247
467
|
const project = snapshot => {
|
|
248
|
-
const definitions = snapshot.definitions().filter(tool => PUBLIC.has(tool.name))
|
|
468
|
+
const definitions = snapshot.definitions().filter(tool => PUBLIC.has(tool.name) || retainedSource && CHROME_SOURCE_NAMES.includes(tool.name)).map(tool=>{
|
|
469
|
+
if(!pages||!['Read','Write','Edit',...(retainedSource?['SourceDraft']:[])].includes(tool.name))return tool;
|
|
470
|
+
const escaped=value=>value.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
471
|
+
const view=structuredClone(tool);view.input_schema.properties.file_path.pattern='^('+escaped(brief.directory)+'/|'+escaped(directory)+'/).+$';
|
|
472
|
+
view.input_schema.properties.file_path.description='Accepted extension source path under '+brief.directory+'/. Host/plugin files are outside this task. '+(brief.fixture?'The fixture '+brief.fixture.file+' is read-only. ':'')+(suppliedDigest?'Supplied read-only inputs: '+brief.supplied_inputs.map(item=>item.file).join(', ')+'. ':'')+'Use existing source observations; reread only an unresolved file.';
|
|
473
|
+
return view;
|
|
474
|
+
});
|
|
249
475
|
const offered = new Set(definitions.map(tool => tool.name));
|
|
250
476
|
return { ...snapshot, definitions: () => definitions,
|
|
251
477
|
execute: (name, input, options = {}) => {
|
|
252
478
|
if (options.modelProposed && !offered.has(name)) throw failure('Use the offered domain actions; administration belongs to the house.', 'HOUSE_SURFACE_CHANGED');
|
|
253
|
-
|
|
479
|
+
const modelSurface = [...offered].filter(name => !options.modelSurface || options.modelSurface.includes(name));
|
|
480
|
+
return snapshot.execute(name, input, options.modelProposed ? {...options,modelSurface} : options);
|
|
254
481
|
}
|
|
255
482
|
};
|
|
256
483
|
};
|
|
484
|
+
const retirePermissionFacts = house.registerPermissionFacts(sessionId, async ({ operation, input, actor, parent_operation, signal }) => {
|
|
485
|
+
if (!pipeline || actor.kind !== 'agent') return null;
|
|
486
|
+
if (sourceEditFacts && ['Write','Edit'].includes(operation)) {
|
|
487
|
+
const native=house.registry.get(operation);
|
|
488
|
+
if (!native || native.contract.origin !== null || native.contract.permission !== operation || native.contract.effects.workspace !== 'write' || ['external','process','authority'].some(key=>native.contract.effects[key]!=='none')) return null;
|
|
489
|
+
// Scope is an observation here, never an authorization. The ordinary
|
|
490
|
+
// source/governance vetoes run again before execution, after the judge.
|
|
491
|
+
const observed=await scope({operation,input,actor,parent_operation,signal});
|
|
492
|
+
if (observed===false || observed?.allowed===false) return null;
|
|
493
|
+
const book=await store.load(sessionId);if(!book.journalAudit.verified)return null;
|
|
494
|
+
const accepted=savedChromeTask(book);if(accepted?.task_digest!==taskDigest)return null;
|
|
495
|
+
const value=await state();
|
|
496
|
+
const baseline=baselineSource?book.rows.findLast(row=>row.type==='chrome_baseline_observed'&&row.source==='workflow_host'&&row.task_digest===taskDigest&&row.source_digest===baselineSource):null;
|
|
497
|
+
const receipt=baseline?book.rows.findLast(row=>row.type==='house_receipt'&&row.operation==='ChromePublish'&&row.status==='returned'&&row.invocation_id===baseline.receipt_id&&row.output_digest===digest(row.output)):null;
|
|
498
|
+
let result;try{if(receipt)result=JSON.parse(receipt.output);}catch{}
|
|
499
|
+
const baselineVerified=Boolean(baseline&&receipt&&result?.task_digest===taskDigest&&result?.source_digest===baselineSource&&value.baseline_observed?.source_digest===baselineSource);
|
|
500
|
+
return {kind:'native_chrome_source_edit_observation',task_digest:taskDigest,governance_plan_hash:plan.plan_hash,project:brief.project,directory:brief.directory,file_path:input.file_path,source_role:'intermediate_implementation_candidate',criteria_origin:accepted.provenance?.criteria_origin||'explicit_host_contract',grants_permission:false,
|
|
501
|
+
source_scope:{status:'observed_in_scope',guard:'runtime:chrome-task.scope',symlink_aliases_excluded:true,immutable_fixture_excluded:true,rechecked_before_execution:true},
|
|
502
|
+
baseline_observation:baselineSource?{required:true,status:baselineVerified?'observed':'unverified',...(baselineVerified?{source_digest:baselineSource,receipt_id:receipt.invocation_id,observation_row:baseline.audit.sequence,observation_sha256:baseline.audit.sha256,passed:baseline.passed}: {})}:{required:false,status:'not_required'},
|
|
503
|
+
native_operation:{name:operation,contract_digest:native.digest,effects:structuredClone(native.contract.effects)},correctness:'unjudged'};
|
|
504
|
+
}
|
|
505
|
+
if (!['Deploy','Open','Interact','Message','Preview'].some(name=>operation===prefix+name) || !['ChromePublish','ChromeTest'].includes(parent_operation)) return null;
|
|
506
|
+
const value = await state();
|
|
507
|
+
if (!value.source_digest || manifestHash(await snapshot(signal)) !== value.source_digest) return null;
|
|
508
|
+
const book = await store.load(sessionId);
|
|
509
|
+
if (!book.journalAudit.verified) return null;
|
|
510
|
+
const accepted = savedChromeTask(book);
|
|
511
|
+
const facts = { kind:'native_chrome_task_resource_binding', task_digest:taskDigest, governance_plan_hash:plan.plan_hash, project:brief.project, directory:brief.directory, source_digest:value.source_digest, criteria_origin:accepted?.provenance?.criteria_origin || 'explicit_host_contract', grants_permission:false };
|
|
512
|
+
if (operation === prefix+'Deploy' && input.project === brief.project && input.directory === brief.directory && input.attempt_id === value.attempt_id) {const observed=book.rows.findLast(r=>r.type==='house_receipt'&&r.invocation_id===value.publication_namespace?.receipt_id&&r.operation===prefix+'Status'&&r.status==='returned'&&r.origin?.plugin==='chrome-lab');const fresh=input.create_only===true&&value.publication_namespace?.create_only&&observed&&!JSON.parse(observed.output).extensions_truncated&&!JSON.parse(observed.output).extensions.some(e=>e.project===brief.project);return { ...facts, publication_attempt:value.attempt_id,...(fresh?{publication_namespace:{status:'new_owned_project',observed_by:observed.invocation_id,observed_at:observed.timestamp}}:{}) };}
|
|
513
|
+
if (operation === prefix+'Open' && input.url === `chrome-extension://${value.deployment?.extension_id}/${brief.popup}` && /^[a-p]{32}$/.test(value.deployment.extension_id)) {
|
|
514
|
+
const publication = book.rows.findLast(r=>r.type==='house_receipt' && [prefix+'Deploy',prefix+'Deployment'].includes(r.operation) && r.status==='returned' && r.origin?.plugin==='chrome-lab' && JSON.parse(r.output).project===brief.project && JSON.parse(r.output).extension_id===value.deployment.extension_id && JSON.parse(r.output).file_manifest_sha256===value.source_digest);
|
|
515
|
+
if (publication) return { ...facts, observation_role:'primary_popup_criterion', criterion:{selector:brief.selector,equals:brief.equals}, ...(brief.fixture?{separate_fixture_criterion:{file:brief.fixture.file,selector:brief.fixture.selector,equals:brief.fixture.equals},page_steps_are_separate:true}:{}), extension_id:value.deployment.extension_id, url:input.url, observed_by:publication.invocation_id, observed_at:publication.timestamp, resource_identity:{status:'verified',extension_id_syntax:'valid_native_check',binding:'exact_project_publication_receipt_and_current_source'} };
|
|
516
|
+
}
|
|
517
|
+
const binding=input.target_id===value.fixture_binding?.target_id?value.fixture_binding:value.binding;
|
|
518
|
+
if (pages && brief.fixture && operation === prefix+'Interact' && parent_operation === 'ChromePublish' && value.stage === 'fixture_reload_pending' && input.action === 'reload' && !input.selector && binding === value.fixture_binding && binding?.identity && binding.project === brief.project && binding.fixture_source_digest === fixtureSource) {
|
|
519
|
+
const preview = book.rows.findLast(r=>r.type==='house_receipt'&&r.operation===prefix+'Preview'&&r.status==='returned'&&r.origin?.plugin==='chrome-lab'&&r.arguments_digest===digest({project:brief.project,file:brief.fixture.file})&&JSON.parse(r.output).target_id===binding.target_id&&JSON.parse(r.output).url===binding.url);
|
|
520
|
+
const observation = book.rows.findLast(r=>r.type==='house_receipt'&&r.operation===prefix+'Contexts'&&r.status==='returned'&&r.origin?.plugin==='chrome-lab'&&JSON.parse(r.output).target_id===binding.target_id&&digest(JSON.parse(r.output).identity)===digest(binding.identity));
|
|
521
|
+
const publication = book.rows.findLast(r=>r.type==='house_receipt'&&r.operation===prefix+'Deploy'&&r.status==='returned'&&r.origin?.plugin==='chrome-lab'&&JSON.parse(r.output).project===brief.project&&JSON.parse(r.output).extension_id===value.deployment?.extension_id&&JSON.parse(r.output).file_manifest_sha256===value.source_digest);
|
|
522
|
+
if (preview && observation && publication) return {...facts,observation_role:'publication_fixture_refresh',resource:binding,observed_by:observation.invocation_id,fixture_opened_by:preview.invocation_id,publication_receipt_id:publication.invocation_id,action:'reload',resource_identity:{status:'verified',binding:'exact_immutable_fixture_preview_current_document_and_own_publication'}};
|
|
523
|
+
}
|
|
524
|
+
if(pages&&brief.fixture&&operation===prefix+'Preview'&&input.project===brief.project&&input.file===brief.fixture.file)return {...facts,fixture:{file:brief.fixture.file,sha256:fixtureSource},action:'open_immutable_local_fixture'};
|
|
525
|
+
if (operation === prefix+'Interact' && binding?.identity && input.target_id === binding.target_id && brief.tests?.some(step=>(step.surface==='page'?value.fixture_binding?.target_id:value.binding?.target_id)===input.target_id&&step.action===input.action && step.selector===input.selector && (step.action!=='fill'||step.text===input.text))) {
|
|
526
|
+
const observation = book.rows.findLast(r=>r.type==='house_receipt' && r.operation===prefix+'Contexts' && r.status==='returned' && JSON.parse(r.output).target_id===input.target_id && digest(JSON.parse(r.output).identity)===digest(binding.identity));
|
|
527
|
+
if (observation) return { ...facts, resource:binding, observed_by:observation.invocation_id, observed_at:observation.timestamp, declared_test_digest:digest(brief.tests), declared_step:brief.tests.find(step=>(step.surface==='page'?value.fixture_binding?.target_id:value.binding?.target_id)===input.target_id&&step.action===input.action && step.selector===input.selector && (step.action!=='fill'||step.text===input.text)), action:input.action, selector:input.selector || null };
|
|
528
|
+
}
|
|
529
|
+
if (operation === prefix+'Message' && value.binding?.identity && input.target_id === value.binding.target_id && brief.tests?.some(step=>step.action==='message'&&step.request===input.request)) {
|
|
530
|
+
const observation=book.rows.findLast(r=>r.type==='house_receipt'&&r.operation===prefix+'Contexts'&&r.status==='returned'&&JSON.parse(r.output).target_id===input.target_id&&digest(JSON.parse(r.output).identity)===digest(value.binding.identity));
|
|
531
|
+
if(observation)return {...facts,resource:value.binding,observed_by:observation.invocation_id,observed_at:observation.timestamp,declared_test_digest:digest(brief.tests),declared_step:brief.tests.find(step=>step.action==='message'&&step.request===input.request),action:'message',request:input.request};
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
});
|
|
257
535
|
const retireObservationRecovery=house.registerRecovery(sessionId,'ChromeRecover',async()=>({kind:'chrome_observation_retry'}));
|
|
258
536
|
const retireRecovery=house.registerRecovery(sessionId,'ChromePublish',async({proposal,executor:recoveryExecutor,signal})=>{
|
|
259
537
|
const before=await store.load(sessionId), checkpointRef=before.rows.findLastIndex(r=>r.type==='chrome_task_checkpoint' && r.source==='workflow_host' && r.task_digest===taskDigest);
|
|
@@ -265,9 +543,36 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
265
543
|
if(observationRef<0 || reconciliationRef<0)throw failure('La reconciliación no quedó asentada.', 'CHROME_TASK_UNCERTAIN');
|
|
266
544
|
return {checkpoint_ref:checkpointRef,observation_ref:observationRef,reconciliation_ref:reconciliationRef};
|
|
267
545
|
});
|
|
268
|
-
return { task: brief, taskDigest, plan,
|
|
269
|
-
system: `Host-managed Chrome task: ${JSON.stringify(brief)}. Policy ${plan.plan_hash}. Work is accepted and active. Read/Edit/Write only its source. ChromePublish deploys and observes; ChromeVerify observes without publication; ${brief.tests ? 'ChromeTest executes the declared behavior tests; ' : ''}ChromeFinish executes the declared check and closes. After a publication/connection interruption, use ChromeRecover to reconcile by reads; never repeat uncertain publication. If it returns recovered, ChromePublish continues without Deploy. Uncertain test interactions remain blocked. No administrative IDs or criteria to supply. Correct failed criteria and publish again. Refusal, uncertainty and pauses are not delivery. Give your final report only after ChromeFinish returns done.`,
|
|
546
|
+
return { task: brief, taskDigest, plan, pinnedSchemas: retainedSource?[...CHROME_SOURCE_NAMES]:[],
|
|
547
|
+
system: `Host-managed Chrome task: ${JSON.stringify(brief)}. ${suppliedDigest?'Supplied inputs are existing immutable assets. Read them if needed; implement only mutable application files. Their identity is enforced by the house, not an instruction to repair them. ':''} Policy ${plan.plan_hash}. Work is accepted and active. Read/Edit/Write only its source. ${retainedSource?'SourceDraft/Retain/Read/Publish are offered directly for bounded source in this task: retain oversized proposals without regeneration, then publish their sealed bytes. This changes local candidate source, not Chrome delivery or permissions. No ToolPack is needed. ':''}${pipeline ? 'After all source files are ready, prefer ChromeDeliver: commit the complete candidate once; the house publishes, tests and closes. If it returns needs_correction, fix source from its evidence and commit again. No separate ChromeTest/ChromeFinish or extra speculative reread is required. ' : ''} ChromePublish deploys and observes; ChromeVerify observes without publication; ${inspection ? 'ChromeInspect reads live own-popup DOM or console without target IDs; observations do not pass checks. ' : ''}${brief.tests ? 'ChromeTest executes the declared behavior tests; ' : ''}ChromeFinish executes the declared check and closes. After a publication/connection interruption, use ChromeRecover to reconcile by reads; never repeat uncertain publication. If it returns recovered, ChromePublish continues without Deploy. Uncertain test interactions remain blocked. No administrative IDs or criteria to supply. Correct failed criteria and publish again. Refusal, uncertainty and pauses are not delivery. Give your final report only after ChromeFinish returns done.`,
|
|
270
548
|
wrap: tools => ({ ...tools, house, policy, workflow: 'chrome-task', definitions: () => project(tools.snapshot()).definitions(), snapshot: () => project(tools.snapshot()), execute: (name,input,options) => project(tools.snapshot()).execute(name,input,options) }),
|
|
549
|
+
async prepareBaseline({signal}={}) {
|
|
550
|
+
if(!baselineSource||(await state()).baseline_observed)return null;
|
|
551
|
+
if(manifestHash(await snapshot(signal))!==baselineSource)throw failure('The initial source changed before its requested observation.','CHROME_TASK_STALE_SOURCE');
|
|
552
|
+
const waiting = async () => {
|
|
553
|
+
const current=await state(),book=await store.load(sessionId);
|
|
554
|
+
if(book.rows.some(r=>r.type==='chrome_baseline_waiting'&&r.attempt_id===current.attempt_id))return null;
|
|
555
|
+
await store.append(sessionId,{type:'chrome_baseline_waiting',source:'workflow_host',task_digest:taskDigest,attempt_id:current.attempt_id,source_digest:baselineSource,asked:'unjudged'});
|
|
556
|
+
return {baseline_completed:false,status:'requires_recovery',attempt_id:current.attempt_id,next_action:'ChromeRecover',note:'The initial publication may have executed. No effect was replayed; reconcile before source edits.'};
|
|
557
|
+
};
|
|
558
|
+
const current=await state();if(current.stage.endsWith('_pending')||current.status==='uncertain')return waiting();
|
|
559
|
+
const callId=randomUUID();let result;
|
|
560
|
+
try { result=JSON.parse(await executor.execute('ChromePublish',{}, {signal,callId})); }
|
|
561
|
+
catch(error){if(signal?.aborted||['HOUSE_REFUSED','HOUSE_GUARD','GOVERNANCE_REFUSED'].includes(error.code))throw error;const after=await state();if(after.stage.endsWith('_pending')||after.status==='uncertain')return waiting();throw error;}
|
|
562
|
+
const book=await store.load(sessionId),receipt=book.rows.findLast(r=>r.type==='house_receipt'&&r.call_id===callId&&r.operation==='ChromePublish'&&r.status==='returned');
|
|
563
|
+
if(!receipt||!(await state()).baseline_observed)throw failure('Initial observation has no durable receipt.','CHROME_TASK_UNCERTAIN');
|
|
564
|
+
await store.append(sessionId,{type:'chrome_baseline_observed',source:'workflow_host',task_digest:taskDigest,source_digest:baselineSource,receipt_id:receipt.invocation_id,passed:result.status==='verified',asked:'unjudged'});
|
|
565
|
+
return {baseline_completed:true,status:result.status,source_digest:baselineSource,receipt_id:receipt.invocation_id,observation:result.observed,diagnostics:result.diagnostics};
|
|
566
|
+
},
|
|
567
|
+
async completedResult(calls = []) {
|
|
568
|
+
if (!pipeline || !calls.some(c=>c.name==='ChromeDeliver')) return null;
|
|
569
|
+
const book = await store.load(sessionId);
|
|
570
|
+
const receipt = book.rows.findLast(r=>r.type==='house_receipt' && r.operation==='ChromeDeliver' && r.status==='returned' && calls.some(c=>c.id===r.call_id&&c.name==='ChromeDeliver'));
|
|
571
|
+
if (!receipt || !book.journalAudit.verified || workProjection(book)?.counts.done !== 1) return null;
|
|
572
|
+
const result = JSON.parse(receipt.output);
|
|
573
|
+
if (result.status !== 'done' || result.task_digest !== taskDigest || manifestHash(await snapshot()) !== result.source_digest) return null;
|
|
574
|
+
return { terminal:true, result:`Chrome candidate verified: ${brief.directory}/${brief.popup}.\nDeclared ${brief.tests ? `${brief.tests.length} behavior steps` : 'DOM criterion'} passed; source snapshot ${result.source_digest}.\nEvidence: ${result.receipt_id}. Human acceptance remains unjudged.`, receipt_id:receipt.invocation_id };
|
|
575
|
+
},
|
|
271
576
|
async finishResult(final, context) {
|
|
272
577
|
let delivery = { status: 'open', task_digest: taskDigest, governance_plan_hash: plan.plan_hash, asked: 'unjudged' };
|
|
273
578
|
const session = await store.load(sessionId);
|
|
@@ -287,6 +592,6 @@ export async function prepareChromeTask({ cwd, house, store, sessionId, executor
|
|
|
287
592
|
final.delivery = delivery;
|
|
288
593
|
await store.append(sessionId, { type: 'chrome_task_delivery', source: 'workflow_host', ...delivery });
|
|
289
594
|
},
|
|
290
|
-
dispose: () => {retireRecovery();retireObservationRecovery(); house.retireGovernance(sessionId); house.registry.replace(OWN, []); }
|
|
595
|
+
dispose: () => {sourceAccess?.dispose();retirePermissionFacts();retireRecovery();retireObservationRecovery(); house.retireGovernance(sessionId); house.registry.replace(OWN, []); }
|
|
291
596
|
};
|
|
292
597
|
}
|