@milpa/comal 0.1.7 → 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.
Files changed (134) hide show
  1. package/README.md +19 -1
  2. package/backend/contracts/remote-commands-v1.json +12 -0
  3. package/governance/chrome-task-v10.json +323 -0
  4. package/governance/chrome-task-v11.json +355 -0
  5. package/governance/chrome-task-v12.json +387 -0
  6. package/governance/chrome-task-v13.json +419 -0
  7. package/governance/chrome-task-v3.json +99 -0
  8. package/governance/chrome-task-v4.json +131 -0
  9. package/governance/chrome-task-v5.json +163 -0
  10. package/governance/chrome-task-v6.json +195 -0
  11. package/governance/chrome-task-v7.json +227 -0
  12. package/governance/chrome-task-v8.json +259 -0
  13. package/governance/chrome-task-v9.json +291 -0
  14. package/governance/source-task-v1.json +1 -0
  15. package/governance/source-task-v2.json +1 -0
  16. package/package.json +14 -1
  17. package/src/agent-recovery-proof.mjs +12 -1
  18. package/src/agent.mjs +94 -30
  19. package/src/audit.mjs +21 -6
  20. package/src/auto-guard.mjs +48 -1
  21. package/src/chrome-candidate-recovery-proof.mjs +47 -0
  22. package/src/chrome-recovery-proof.mjs +6 -2
  23. package/src/chrome-source-syntax.mjs +93 -0
  24. package/src/cli.mjs +158 -54
  25. package/src/codex-provider.mjs +3 -2
  26. package/src/connection-settings.mjs +204 -0
  27. package/src/context-pressure.mjs +2 -1
  28. package/src/durable-agent-runs.mjs +5 -11
  29. package/src/event-channels.mjs +8 -0
  30. package/src/file-recovery-proof.mjs +11 -3
  31. package/src/file-recovery.mjs +24 -0
  32. package/src/gemini-provider.mjs +6 -1
  33. package/src/git-agent-revision.mjs +12 -0
  34. package/src/git-agent-workspaces.mjs +143 -0
  35. package/src/git-integration-lineage.mjs +20 -0
  36. package/src/git-integration-operations.mjs +200 -0
  37. package/src/git-integration-recovery-proof.mjs +70 -0
  38. package/src/git-page-cli-bridge.mjs +61 -0
  39. package/src/git-page-evidence.mjs +30 -0
  40. package/src/git-page-identity.mjs +10 -0
  41. package/src/git-page-proof.mjs +96 -0
  42. package/src/git-page-settlement.mjs +21 -0
  43. package/src/git-page-verification.mjs +134 -0
  44. package/src/git-plumbing.mjs +212 -0
  45. package/src/git-process-worker.mjs +50 -0
  46. package/src/git-source.mjs +110 -0
  47. package/src/git-workspaces.mjs +519 -0
  48. package/src/goals.mjs +13 -7
  49. package/src/house.mjs +246 -20
  50. package/src/i18n/cli-es.mjs +4 -0
  51. package/src/i18n/es.mjs +35 -0
  52. package/src/judges.mjs +27 -11
  53. package/src/mcp-runtime.mjs +15 -3
  54. package/src/model-controls.mjs +11 -1
  55. package/src/model-deadline.mjs +4 -3
  56. package/src/native-refusal.mjs +38 -0
  57. package/src/onboarding.mjs +88 -24
  58. package/src/openai-provider.mjs +14 -7
  59. package/src/operations.mjs +9 -2
  60. package/src/output-recovery.mjs +48 -0
  61. package/src/peer-tools.mjs +18 -8
  62. package/src/permission-controls.mjs +232 -0
  63. package/src/permission-rules.mjs +312 -0
  64. package/src/permissions.mjs +113 -30
  65. package/src/plugin-catalog.mjs +84 -0
  66. package/src/plugin-config.mjs +48 -6
  67. package/src/plugin-controls.mjs +332 -0
  68. package/src/plugin-notices.mjs +88 -0
  69. package/src/plugin-trust.mjs +104 -0
  70. package/src/plugins/chrome-lab/bridge.mjs +38 -12
  71. package/src/plugins/chrome-lab/client.mjs +103 -25
  72. package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
  73. package/src/plugins/chrome-lab/index.mjs +116 -26
  74. package/src/plugins/chrome-lab/project.mjs +30 -2
  75. package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
  76. package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
  77. package/src/plugins/open-relay/actions.mjs +205 -0
  78. package/src/plugins/open-relay/bound.mjs +47 -0
  79. package/src/plugins/open-relay/client.mjs +158 -0
  80. package/src/plugins/open-relay/dossier.mjs +119 -0
  81. package/src/plugins/open-relay/index.mjs +153 -0
  82. package/src/plugins/open-relay/lookup.mjs +45 -0
  83. package/src/plugins/open-relay/notices.mjs +79 -0
  84. package/src/plugins/open-relay/watch.mjs +127 -0
  85. package/src/plugins.mjs +72 -29
  86. package/src/private-fs.mjs +40 -0
  87. package/src/process-lock.mjs +24 -13
  88. package/src/provider-auth.mjs +14 -3
  89. package/src/provider.mjs +11 -2
  90. package/src/read-only-check.mjs +7 -3
  91. package/src/scope-ceiling.mjs +56 -0
  92. package/src/scoped-policy.mjs +39 -0
  93. package/src/secret-login.mjs +68 -0
  94. package/src/session-bus.mjs +41 -7
  95. package/src/session-projections-candidate.mjs +119 -0
  96. package/src/session-projections.mjs +119 -0
  97. package/src/session-read-cache.mjs +23 -3
  98. package/src/sessions.mjs +35 -11
  99. package/src/source-editor-role.mjs +13 -0
  100. package/src/source-ownership.mjs +73 -0
  101. package/src/source-proposal-proof.mjs +55 -0
  102. package/src/source-proposals.mjs +118 -0
  103. package/src/source-requests.mjs +148 -0
  104. package/src/source-task-proof.mjs +24 -0
  105. package/src/source-task-revision-proof.mjs +44 -0
  106. package/src/subagents.mjs +187 -87
  107. package/src/tool-packs.mjs +5 -5
  108. package/src/tools.mjs +82 -15
  109. package/src/trusted-context.mjs +5 -1
  110. package/src/tui/connection-transition.mjs +1 -1
  111. package/src/tui/controller.mjs +8 -4
  112. package/src/tui/discovery.mjs +24 -12
  113. package/src/tui/inbound-blocks.mjs +55 -0
  114. package/src/tui/present.mjs +504 -0
  115. package/src/tui/screen.mjs +49 -13
  116. package/src/windows-private.ps1 +61 -0
  117. package/src/work-assertions.mjs +6 -2
  118. package/src/work-plan.mjs +34 -8
  119. package/src/work-state.mjs +15 -9
  120. package/src/workflows/chrome-page-cases.mjs +156 -0
  121. package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
  122. package/src/workflows/chrome-page-diagnostics.mjs +113 -0
  123. package/src/workflows/chrome-page-law.mjs +48 -0
  124. package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
  125. package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
  126. package/src/workflows/chrome-page-task.mjs +429 -0
  127. package/src/workflows/chrome-page-verifier.mjs +123 -0
  128. package/src/workflows/chrome-source.mjs +73 -0
  129. package/src/workflows/chrome-supplied-inputs.mjs +43 -0
  130. package/src/workflows/chrome-task-adoption.mjs +122 -0
  131. package/src/workflows/chrome-task.mjs +379 -74
  132. package/src/workflows/source-task-v1.mjs +124 -0
  133. package/src/workflows/source-task-v2.mjs +227 -0
  134. package/src/workflows/source-task.mjs +13 -0
@@ -0,0 +1,73 @@
1
+ import { digest } from '../operations.mjs';
2
+ import { sourceInput, sourceOwn, nativeSourceParent } from '../source-proposal-proof.mjs';
3
+ import { workProjection } from '../work-plan.mjs';
4
+
5
+ export const CHROME_SOURCE_NAMES = Object.freeze(['SourceDraft', 'SourceRetain', 'SourceRead', 'SourcePublish']);
6
+
7
+ // A host binding narrows the existing native source protocol; it grants no writer authority.
8
+ export function bindChromeSource({ executor, store, sessionId, cwd, plan, taskDigest, pathAllowed, directory }) {
9
+ if (!executor.bindSourceScope || !executor.identity || !CHROME_SOURCE_NAMES.every(name => executor.isNativeOperation?.(name)) || !executor.isNativeOperation('FileInfo')) {
10
+ throw Object.assign(new Error('Chrome source recovery requires the original native source ports and FileInfo.'), { code: 'CHROME_TASK_SOURCE_UNAVAILABLE' });
11
+ }
12
+ const identity = executor.identity();
13
+ const owner = view => view.session_id === sessionId && view.surface === identity.surface && digest(view.actor) === digest(identity.actor);
14
+ const accepted = book => {
15
+ const index = book.rows.findIndex(row => row.type === 'chrome_task_accepted' && row.task_digest === taskDigest && row.governance_plan_hash === plan.plan_hash);
16
+ return index >= 0 ? index : null;
17
+ };
18
+ const currentScope = book => { const work = workProjection(book); return { reset_ref: book.rows.findLastIndex(row => row.type === 'reset'), goal_id: book.goal?.id ?? null,
19
+ task_id: work?.current?.id ?? null, work_digest: work?.current?.work_digest ?? null }; };
20
+ const original = (book, callId) => {
21
+ const index = accepted(book); if (index === null) return null;
22
+ const proposals = book.rows.filter(row => row.type === 'house_proposal' && row.call_id === callId && sourceOwn(row, { ...identity, house: cwd }));
23
+ if (proposals.length !== 1) return null;
24
+ const row = proposals[0], input = sourceInput(book.rows, row), receipt = book.rows.findLast(item => item.type === 'house_receipt' && item.invocation_id === row.invocation_id);
25
+ const before=book.rows.slice(0,book.rows.indexOf(row)),originScope=currentScope({id:book.id,cwd:book.cwd,rows:before,goal:before.findLast(item=>item.type==='goal')?.goal});
26
+ return book.rows.indexOf(row) > index && digest(originScope)===digest(currentScope(book)) && row.governance_plan_hash === plan.plan_hash && ['Write', 'Edit'].includes(row.operation) && input && receipt?.code === 'MODEL_STEP_TOO_LARGE' && receipt.status === 'refused' && receipt.effects_observed === 'none' ? { row, input } : null;
27
+ };
28
+ const binding = executor.bindSourceScope(sessionId, async (data, options, book) => {
29
+ const acceptedIndex=accepted(book);
30
+ if (acceptedIndex === null || currentScope(book).task_id !== 'chrome-task' || digest(data.scope) !== digest(currentScope(book))) return false;
31
+ if(book.goal&&['cleared','met','failed','budget'].includes(book.goal.status))return false;
32
+ const proposal = book.rows.find(row => row.type === 'house_proposal' && row.invocation_id === options.invocation_id);
33
+ if (!proposal || !owner(proposal) || book.rows.indexOf(proposal)<=acceptedIndex || proposal.governance_plan_hash!==plan.plan_hash) return false;
34
+ if(data.inspection_id){const receipt=book.rows.find(row=>row.type==='house_receipt'&&row.invocation_id===data.inspection_id);if(!receipt||book.rows.indexOf(receipt)<=acceptedIndex||receipt.governance_plan_hash!==plan.plan_hash)return false;}
35
+ if (data.original_invocation) {
36
+ const old = book.rows.find(row => row.type === 'house_proposal' && row.invocation_id === data.original_invocation);
37
+ if (!old || !original(book, old.call_id)) return false;
38
+ }
39
+ const allowed=await pathAllowed(data.requested_path, true),fresh=await store.load(sessionId);
40
+ return allowed&&digest(currentScope(fresh))===digest(data.scope)&&accepted(fresh)===acceptedIndex&&!(fresh.goal&&['cleared','met','failed','budget'].includes(fresh.goal.status));
41
+ });
42
+ const syntax = directory === undefined ? null : executor.bindChromeSourceSyntax?.(sessionId, { directory, task_digest: taskDigest });
43
+ return {
44
+ active: binding.active, dispose() { binding.dispose(); syntax?.dispose(); },
45
+ async admit(view) {
46
+ if (!binding.active() || !owner(view) || !executor.isNativeOperation(view.operation)) return false;
47
+ if (view.operation === 'SourceDraft') return pathAllowed(view.input.file_path, true);
48
+ if (view.operation === 'SourceRetain') {
49
+ const item = original(await store.load(sessionId), view.input.call_id);
50
+ return Boolean(item && await pathAllowed(item.input.file_path, true));
51
+ }
52
+ return true; // Opaque refs/listing are filtered inside the native handler before any output.
53
+ },
54
+ async inspection(view) {
55
+ if (!binding.active() || !owner(view) || !executor.isNativeOperation('FileInfo')) return false;
56
+ const book = await store.load(sessionId), rows = book.rows;
57
+ const parent = rows.find(row => row.type === 'house_proposal' && row.invocation_id === view.parent_invocation_id);
58
+ if (!parent || !['SourceDraft', 'SourceRetain'].includes(parent.operation) || !nativeSourceParent(rows, parent) || !owner(parent) || parent.governance_plan_hash !== plan.plan_hash || !executor.isNativeOperation(parent.operation)) return false;
59
+ const acceptedIndex=accepted(book);if(acceptedIndex===null||rows.indexOf(parent)<=acceptedIndex)return false;
60
+ if (rows.some(row => row.type === 'house_receipt' && row.invocation_id === parent.invocation_id)) return false;
61
+ const intents = rows.filter(row => row.type === 'source_inspection_intent' && row.source === 'source_host' && row.invocation_id === parent.invocation_id && sourceOwn(row, parent));
62
+ const intent = intents.length === 1 && intents[0];
63
+ if (!intent || intent.input_digest !== digest(intent.input) || digest(view.input) !== intent.input_digest || digest(view.input) !== digest({ file_path: intent.input.file_path, hash: true, allow_missing: true, max_bytes: 1048576 })) return false;
64
+ if (!rows.some(row => row.type === 'house_started' && row.invocation_id === parent.invocation_id && rows.indexOf(row) < rows.indexOf(intent))) return false;
65
+ if (parent.operation === 'SourceRetain') {
66
+ const input = sourceInput(rows, parent);
67
+ // Actual model calls carry their exact input. Native host callers still bind through the inspection intent.
68
+ if (input && original(book, input.call_id)?.input.file_path !== view.input.file_path) return false;
69
+ }
70
+ return pathAllowed(view.input.file_path, true);
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,43 @@
1
+ import {lstat,realpath,open} from 'node:fs/promises';
2
+ import {constants} from 'node:fs';
3
+ import {createHash} from 'node:crypto';
4
+ import path from 'node:path';
5
+ import {safeRelative} from '../plugins/chrome-lab/project.mjs';
6
+
7
+ export const suppliedInputsSchema={type:'array',minItems:0,maxItems:32,description:'Existing read-only assets supplied to this accepted extension. An empty list explicitly selects the same policy without supplied assets. Exact bytes must already exist; this grants no permission or package installation. The house preserves their hashes across source changes, publication and resume.',items:{type:'object',additionalProperties:false,required:['file','bytes','sha256'],properties:{file:{type:'string',minLength:1,maxLength:220},bytes:{type:'integer',minimum:0,maximum:1048576},sha256:{type:'string',pattern:'^[a-f0-9]{64}$'}}}};
8
+ const failure=(file,reason)=>Object.assign(new Error('Supplied input '+file+' '+reason+'. Restore its accepted bytes; no publication was authorized by this check.'),{code:'CHROME_TASK_INPUT_CHANGED',details:{file,reason}});
9
+ export function normalizeSuppliedInputs(items){
10
+ const paths=new Set();let total=0;
11
+ for(const item of items){
12
+ safeRelative(item.file);const name=item.file.toLowerCase();
13
+ if(name.split('/').some(part=>['.git','.comal','.harness','.ssh','node_modules'].includes(part)||/^\.env(?:\.|$)/i.test(part)))throw failure(item.file,'uses a protected path');
14
+ if([...paths].some(other=>name===other||name.startsWith(other+'/')||other.startsWith(name+'/')))throw failure(item.file,'duplicates or aliases another supplied path');paths.add(name);total+=item.bytes;
15
+ }
16
+ if(total>10000000)throw failure('(manifest)','exceeds the aggregate byte limit');
17
+ return items;
18
+ }
19
+ export function isSuppliedPath(relative,items){
20
+ const value=relative.split(path.sep).join('/').toLowerCase();
21
+ return items.some(({file})=>{const name=file.toLowerCase();return name===value||name.startsWith(value+'/')||value.startsWith(name+'/');});
22
+ }
23
+ // Stable bounded reads bind content, not a permanent inode. No symlink/hardlink aliases.
24
+ export async function verifySuppliedInputs(directory,items,{signal}={}){
25
+ for(const item of items){
26
+ signal?.throwIfAborted();const file=path.join(directory,safeRelative(item.file));let handle;
27
+ try{
28
+ const before=await lstat(file);
29
+ if(!before.isFile()||before.nlink!==1||before.size!==item.bytes||await realpath(file)!==file)throw failure(item.file,'is missing, redirected or has a different size');
30
+ handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0)|(constants.O_NONBLOCK||0));
31
+ const opened=await handle.stat();if(opened.ino!==before.ino||opened.dev!==before.dev||!opened.isFile())throw failure(item.file,'changed before opening');
32
+ const buffer=Buffer.alloc(item.bytes+1);let offset=0;
33
+ while(offset<buffer.length){signal?.throwIfAborted();const {bytesRead}=await handle.read(buffer,offset,buffer.length-offset,offset);if(!bytesRead)break;offset+=bytesRead;}
34
+ const after=await handle.stat(),current=await lstat(file);
35
+ if(offset!==item.bytes||[after,current].some(s=>!s.isFile()||s.nlink!==1||s.dev!==before.dev||s.ino!==before.ino||s.size!==before.size||s.mtimeMs!==before.mtimeMs||s.ctimeMs!==before.ctimeMs)||await realpath(file)!==file)throw failure(item.file,'changed while reading');
36
+ if(createHash('sha256').update(buffer.subarray(0,offset)).digest('hex')!==item.sha256)throw failure(item.file,'has a different digest');
37
+ }catch(error){if(signal?.aborted)throw error;if(error.code==='CHROME_TASK_INPUT_CHANGED')throw error;throw failure(item.file,'cannot be read as its accepted regular file');}
38
+ finally{await handle?.close();}
39
+ }
40
+ }
41
+ export function assertSuppliedSnapshot(snapshot,items){
42
+ for(const item of items){const actual=snapshot.files.find(file=>file.path===item.file);if(!actual||actual.sha256!==item.sha256||Buffer.from(actual.data,'base64').length!==item.bytes)throw failure(item.file,'differs in the publication snapshot');}
43
+ }
@@ -0,0 +1,122 @@
1
+ import { digest } from '../operations.mjs';
2
+ import { workProjection } from '../work-plan.mjs';
3
+ import { chromeTaskSchema, normalizeTask, savedChromeTask, validateTaskDirectory, chromeFixtureDigest } from './chrome-task.mjs';
4
+ import { verifySuppliedInputs } from './chrome-supplied-inputs.mjs';
5
+ import { snapshotProject } from '../plugins/chrome-lab/project.mjs';
6
+ import { isHumanIntentRecord } from '../trusted-context.mjs';
7
+
8
+ const required = ['Deploy','Deployment','Open','Targets','Assert','Contexts','Interact','ReadDOM','Console'];
9
+ const entryNames = new Set(['ChromeTask','Read','Glob','Grep','ListDirectory','FileInfo','WebSearch','WebFetch']);
10
+ const fail = (message, code = 'CHROME_TASK_DECLARATION') => Object.assign(new Error(message), { code });
11
+ const activeSkill = session => session.rows.some(row => row.type === 'skill_scope_started' && row.source === 'skill_host' && !session.rows.some(end => end.type === 'skill_scope_completed' && end.source === 'skill_host' && end.scope_id === row.scope_id));
12
+ const declarationSchema = structuredClone(chromeTaskSchema);
13
+ // Conversational declaration retains its historical popup defaults. A page-only
14
+ // verifier is selected explicitly by the host at startup, never by model input.
15
+ delete declarationSchema.allOf;
16
+ delete declarationSchema.properties.surface;
17
+ delete declarationSchema.properties.verification;
18
+ declarationSchema.required = ['selector', 'equals'];
19
+ delete declarationSchema.properties.project;
20
+ declarationSchema.properties.directory.description = 'Workspace source folder, default extension. It may be new; Write creates files under ordinary permissions.';
21
+ declarationSchema.properties.popup.description = 'Popup file relative to the source folder, default popup.html.';
22
+ declarationSchema.properties.tests.description = 'Functional checks derived from the request, in order: fill/click/reload/assert. Include persistence and deletion when requested. Assertions need equals or count. Tests stay immutable after adoption.';
23
+ // Disjoint variants show the required arguments of each action explicitly.
24
+ const step = (action, properties, required) => ({ type: 'object', properties: { action: { const: action }, ...properties, surface: fields.surface }, required: ['action', ...required], additionalProperties: false });
25
+ const fields = chromeTaskSchema.properties.tests.items.properties;
26
+ // llama.cpp #27087 rejects nested char{0,2000}. This redundant equivalent
27
+ // pattern uses 0..1999 plus one optional character, preserving maxLength:2000.
28
+ // Dot plus the four ECMAScript line terminators covers every Unicode character.
29
+ // Avoid two variable chunks: their ambiguous splits made live decoding stall.
30
+ const boundedText = field => ({ ...field, pattern: '^(.|\\n|\\r|\\u2028|\\u2029){0,1999}(.|\\n|\\r|\\u2028|\\u2029)?$' });
31
+ declarationSchema.properties.fixture.properties.equals=boundedText(declarationSchema.properties.fixture.properties.equals);
32
+ declarationSchema.properties.tests.items = { oneOf: [
33
+ step('click', { selector: fields.selector }, ['selector']),
34
+ step('fill', { selector: fields.selector, text: boundedText(fields.text) }, ['selector', 'text']),
35
+ step('reload', {}, []),
36
+ step('message', {request:fields.request,equals:boundedText(fields.equals)}, ['request','equals']),
37
+ step('assert', { selector: fields.selector, equals: boundedText(fields.equals), count: fields.count }, ['selector', 'equals']),
38
+ step('assert', { selector: fields.selector, count: fields.count }, ['selector', 'count'])
39
+ ] };
40
+
41
+ // Domain contracts are declared through House, then activated only after their
42
+ // receipt closes. A model never receives the host executor or policy controls.
43
+ export class ChromeTaskAdoption {
44
+ constructor({ cwd, house, store, sessionId, coordinated = true }) { Object.assign(this, { cwd, house, store, sessionId, coordinated }); }
45
+ id() { return typeof this.sessionId === 'function' ? this.sessionId() : this.sessionId; }
46
+ // COMAL_DISABLE_CHROME_TASK=1 deja chrome-lab en crudo, sin ofrecer ChromeTask (lo usa el banco E3).
47
+ available() { return process.env.COMAL_DISABLE_CHROME_TASK !== '1' && required.every(name => this.house.registry.get('plugin__chrome-lab__' + name)); }
48
+ async sync() {
49
+ const session = await this.store.load(this.id());
50
+ this.pendingDeclaration = !savedChromeTask(session) && session.rows.some(row => row.type === 'chrome_task_proposed' && row.source === 'workflow_host');
51
+ const available = this.available() && !savedChromeTask(session) && !workProjection(session) && !activeSkill(session);
52
+ this.focused = Boolean(this.coordinated && available);
53
+ if (!available) { this.house.registry.replace(['ChromeTask'], []); return; }
54
+ const deploy = this.house.registry.get('plugin__chrome-lab__Deploy');
55
+ const capabilities = digest({deploy:deploy.digest,message:this.house.registry.get('plugin__chrome-lab__Message')?.digest || null,preview:this.house.registry.get('plugin__chrome-lab__Preview')?.digest || null});
56
+ if (this.house.registry.get('ChromeTask') && this.deployDigest === capabilities) return;
57
+ this.house.registry.replace(['ChromeTask'], []); this.deployDigest = capabilities;
58
+ const inputSchema = structuredClone(declarationSchema);
59
+ if (!this.house.registry.get('plugin__chrome-lab__Message')) inputSchema.properties.tests.items.oneOf=inputSchema.properties.tests.items.oneOf.filter(variant=>variant.properties.action.const!=='message');
60
+ if(!this.house.registry.get('plugin__chrome-lab__Preview')){delete inputSchema.properties.fixture;for(const variant of inputSchema.properties.tests.items.oneOf)delete variant.properties.surface;}
61
+ this.house.registry.add({ name: 'ChromeTask', description: 'Start coordinated Chrome extension work directly from the user request, BEFORE WorkPlanSet or effects. Declare the DOM criterion and functional behavior tests; no task file, plan versions or target IDs. Emit ALONE and await the next offered domain tools. The house assigns project identity, publishes, binds targets, runs checks and closes. Criteria are your proposal, not proof of user acceptance. No permission grant.',
62
+ input_schema: inputSchema, steps: true,
63
+ effects: { workspace: 'read', external: 'none', process: 'none', authority: 'none' },
64
+ run: async (input, options) => {
65
+ if (options.sessionId !== this.id() || options.actor.kind !== 'agent') throw fail('Only the current root resident can declare this session contract.');
66
+ const session = await this.store.load(this.id());
67
+ if (!session.journalAudit.verified) throw fail('Invalid session journal.', 'AUDIT_INVALID');
68
+ if (!this.available()) throw fail('Chrome Lab is not active; load its configured plugin before declaring a task.');
69
+ if (activeSkill(session)) throw fail('Declare the coordinated task in the resident session before entering a temporary skill scope. No contract or tool scope changed.', 'CHROME_TASK_SCOPE');
70
+ if (savedChromeTask(session) || workProjection(session)) throw fail('Existing work is preserved. Continue its contract; start a new session for a different Chrome task.');
71
+ const task = normalizeTask({ directory: 'extension', popup: 'popup.html', ...input, project: 'comal-' + this.id().replaceAll('-', '').slice(0, 24) });
72
+ this.house.registry.get('plugin__chrome-lab__Deploy').validate({ project: task.project, directory: task.directory });
73
+ const directory=await validateTaskDirectory(this.cwd, task.directory);
74
+ if(task.supplied_inputs)await verifySuppliedInputs(directory,task.supplied_inputs);
75
+ const baselineSource=task.baseline?digest((await snapshotProject(this.cwd,task.directory)).files.map(({path,sha256})=>({path,sha256}))):null;
76
+ const ref = session.rows.findLastIndex(row => isHumanIntentRecord(row) && row.text.trim());
77
+ const goalRef = session.rows.findLastIndex(row => row.type === 'goal' && row.goal?.status === 'active');
78
+ const requestRef = ref >= 0 ? ref : goalRef;
79
+ if (requestRef < 0) throw fail('A recorded user request or active goal is required; tool data cannot supply authority.');
80
+ const previous = session.rows.findLast(row => row.type === 'chrome_task_proposed' && row.source === 'workflow_host');
81
+ if (previous && digest(previous.task) !== digest(task)) throw fail('A different Chrome declaration is pending; its criteria cannot be replaced silently.');
82
+ await options.recordWork({ type: 'chrome_task_proposed', source: 'workflow_host', task, task_digest: digest(task), provenance: {
83
+ criteria_origin: 'resident_proposal', request_ref: requestRef, request_sha256: session.rows[requestRef].audit.sha256,
84
+ ...(baselineSource?{baseline_source_digest:baselineSource}:{}),
85
+ ...(task.supplied_inputs?{supplied_inputs_digest:digest(task.supplied_inputs)}:{}),
86
+ ...(task.fixture?{fixture_source_digest:await chromeFixtureDigest(this.cwd,task)}:{}),
87
+ request_kind: session.rows[requestRef].type, human_acceptance: 'unjudged'
88
+ } });
89
+ this.pendingDeclaration = true;
90
+ return JSON.stringify({ status: 'declared', task, task_digest: digest(task), next_action: 'The house activates this immutable contract before the next model call. Use the newly offered source/domain tools; no WorkPlanSet, ToolPack, task file or target IDs.', grants_permission: false });
91
+ }
92
+ });
93
+ }
94
+ async pending() {
95
+ const session = await this.store.load(this.id());
96
+ if (savedChromeTask(session)) return null;
97
+ const ref = session.rows.findLastIndex(row => row.type === 'chrome_task_proposed' && row.source === 'workflow_host');
98
+ if (ref < 0) return null;
99
+ if (!session.journalAudit.verified) throw fail('Invalid session journal.', 'AUDIT_INVALID');
100
+ const row = session.rows[ref], receipt = session.rows.findLast(item => item.type === 'house_receipt' && item.invocation_id === row.invocation_id && item.operation === 'ChromeTask');
101
+ if (!receipt || receipt.status !== 'returned') throw fail('The declaration has no completed receipt. Preserve the journal and inspect the interrupted declaration; no publication was attempted.', 'CHROME_TASK_DECLARATION_UNCERTAIN');
102
+ if (row.task_digest !== digest(row.task) || session.rows[row.provenance.request_ref]?.audit?.sha256 !== row.provenance.request_sha256) throw fail('The declaration no longer matches its original request.', 'AUDIT_INVALID');
103
+ return { task: row.task, source: 'conversation_declaration', provenance: { ...row.provenance, proposal_ref: ref, proposal_sha256: row.audit.sha256, receipt_id: receipt.invocation_id } };
104
+ }
105
+ wrap(tools) {
106
+ const owner = this;
107
+ const snapshot = () => {
108
+ const captured = tools.snapshot();
109
+ const focused = this.focused;
110
+ const project = definitions => focused ? definitions.filter(op => entryNames.has(op.name)).sort((a,b) => Number(b.name === 'ChromeTask') - Number(a.name === 'ChromeTask')) : definitions;
111
+ const definitions = project(captured.definitions()), offered = new Set(definitions.map(op => op.name));
112
+ return { ...captured, definitions: () => definitions, catalogue: () => project(captured.catalogue()), execute: async (name, input, options = {}) => {
113
+ // A mixed response cannot start another effect using the catalogue from
114
+ // before declaration. Host activation uses the raw executor, at boundary.
115
+ if (options.modelProposed && name !== 'ChromeTask' && this.pendingDeclaration && await this.pending()) throw fail('ChromeTask was declared in this batch. No further call executed; continue with the next domain catalogue.', 'CHROME_TASK_BOUNDARY');
116
+ if (options.modelProposed && focused && !offered.has(name)) throw fail('Declare ChromeTask first; this entry surface permits only source inspection and declaration. No effect executed.', 'CHROME_TASK_ENTRY');
117
+ return captured.execute(name, input, options);
118
+ } };
119
+ };
120
+ return { ...tools, get workflowEntry() { return owner.focused; }, snapshot, definitions: () => snapshot().definitions(), catalogue: () => snapshot().catalogue(), execute: (name, input, options) => snapshot().execute(name, input, options) };
121
+ }
122
+ }