@milpa/comal 0.1.0 → 0.1.2
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 +30 -28
- package/backend/contracts/remote-commands-v1.json +125 -0
- package/bin/comal.mjs +16 -5
- package/docs/investigation.md +2 -2
- package/governance/chrome-task-v2.json +67 -0
- package/node_modules/@jitl/quickjs-ffi-types/LICENSE +21 -0
- package/node_modules/@jitl/quickjs-ffi-types/README.md +5 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.mts +549 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.ts +549 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.js +2 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.js.map +1 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs +2 -0
- package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-ffi-types/package.json +36 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/LICENSE +47 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/README.md +93 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.cjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm +0 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.mts +94 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.ts +94 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.mts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.ts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/package.json +61 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/LICENSE +47 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/README.md +88 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.mjs +5430 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cjs +5485 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.cjs +5424 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs +5541 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm +0 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.mts +85 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.ts +85 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.mts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.ts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-debug-sync/package.json +61 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/LICENSE +47 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/README.md +89 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.mjs +26 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cjs +25 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.cjs +25 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.d.ts +15 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.mjs +30 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.wasm +0 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.mts +94 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.ts +94 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.mts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.ts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/package.json +61 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/LICENSE +47 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/README.md +82 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.mjs +22 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cjs +21 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.d.ts +11 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.mjs +25 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm +0 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.mts +85 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.ts +85 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.mts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.ts +20 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs +2 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs.map +1 -0
- package/node_modules/@jitl/quickjs-wasmfile-release-sync/package.json +61 -0
- package/node_modules/quickjs-emscripten/LICENSE +21 -0
- package/node_modules/quickjs-emscripten/README.md +928 -0
- package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs +2 -0
- package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs.map +1 -0
- package/node_modules/quickjs-emscripten/dist/index.d.mts +57 -0
- package/node_modules/quickjs-emscripten/dist/index.d.ts +57 -0
- package/node_modules/quickjs-emscripten/dist/index.global.js +7309 -0
- package/node_modules/quickjs-emscripten/dist/index.js +2 -0
- package/node_modules/quickjs-emscripten/dist/index.js.map +1 -0
- package/node_modules/quickjs-emscripten/dist/index.mjs +2 -0
- package/node_modules/quickjs-emscripten/dist/index.mjs.map +1 -0
- package/node_modules/quickjs-emscripten/dist/variants.d.mts +39 -0
- package/node_modules/quickjs-emscripten/dist/variants.d.ts +39 -0
- package/node_modules/quickjs-emscripten/dist/variants.js +2 -0
- package/node_modules/quickjs-emscripten/dist/variants.js.map +1 -0
- package/node_modules/quickjs-emscripten/dist/variants.mjs +2 -0
- package/node_modules/quickjs-emscripten/dist/variants.mjs.map +1 -0
- package/node_modules/quickjs-emscripten/package.json +69 -0
- package/node_modules/quickjs-emscripten-core/LICENSE +21 -0
- package/node_modules/quickjs-emscripten-core/README.md +341 -0
- package/node_modules/quickjs-emscripten-core/README.template.md +70 -0
- package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs +2 -0
- package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs.map +1 -0
- package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs +7 -0
- package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
- package/node_modules/quickjs-emscripten-core/dist/index.d.mts +2033 -0
- package/node_modules/quickjs-emscripten-core/dist/index.d.ts +2033 -0
- package/node_modules/quickjs-emscripten-core/dist/index.js +7 -0
- package/node_modules/quickjs-emscripten-core/dist/index.js.map +1 -0
- package/node_modules/quickjs-emscripten-core/dist/index.mjs +2 -0
- package/node_modules/quickjs-emscripten-core/dist/index.mjs.map +1 -0
- package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs +2 -0
- package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs.map +1 -0
- package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs +2 -0
- package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
- package/node_modules/quickjs-emscripten-core/package.json +49 -0
- package/package.json +6 -2
- package/src/agent.mjs +17 -6
- package/src/artifact-tools.mjs +3 -2
- package/src/auto-guard.mjs +5 -1
- package/src/cli.mjs +267 -201
- package/src/cloud-client.mjs +58 -8
- package/src/code-run-worker.mjs +70 -0
- package/src/code-run.mjs +97 -0
- package/src/codex-provider.mjs +28 -28
- package/src/compaction.mjs +1 -1
- package/src/goals.mjs +2 -2
- package/src/house.mjs +24 -24
- package/src/i18n/cli-es.mjs +97 -0
- package/src/i18n/es.mjs +227 -0
- package/src/i18n/index.mjs +48 -0
- package/src/image-attachments.mjs +263 -0
- package/src/model-controls.mjs +4 -4
- package/src/onboarding.mjs +91 -0
- package/src/openai-provider.mjs +24 -24
- package/src/peer-tools.mjs +4 -3
- package/src/permissions.mjs +53 -17
- package/src/provider-auth.mjs +30 -30
- package/src/provider-profiles.mjs +27 -27
- package/src/provider.mjs +30 -30
- package/src/remote-commands.mjs +11 -0
- package/src/remote-control.mjs +180 -0
- package/src/sessions.mjs +3 -3
- package/src/subagents.mjs +7 -3
- package/src/tool-packs.mjs +5 -2
- package/src/tools.mjs +92 -34
- package/src/tui/brand.mjs +2 -1
- package/src/tui/clipboard.mjs +5 -4
- package/src/tui/composer.mjs +77 -11
- package/src/tui/controller.mjs +111 -29
- package/src/tui/discovery.mjs +105 -100
- package/src/tui/markdown.mjs +10 -9
- package/src/tui/message-queue.mjs +27 -9
- package/src/tui/screen.mjs +209 -118
- package/src/vision.mjs +31 -3
- package/src/work-assertions.mjs +7 -7
- package/src/work-plan.mjs +189 -21
- package/src/work-recovery-proof.mjs +13 -1
- package/src/workflows/chrome-task.mjs +24 -7
package/src/cloud-client.mjs
CHANGED
|
@@ -8,17 +8,67 @@ import { networkFetch } from '../backend/verifier/transport.mjs';
|
|
|
8
8
|
|
|
9
9
|
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
10
|
const defaultOrigin='https://comal.milpahq.com';
|
|
11
|
-
export function cloudOrigin(value=defaultOrigin,env=process.env){const url=new URL(value);if(url.username||url.password||url.pathname!=='/'||url.search||url.hash)throw Error('Comal Cloud
|
|
11
|
+
export function cloudOrigin(value=defaultOrigin,env=process.env){const url=new URL(value);if(url.username||url.password||url.pathname!=='/'||url.search||url.hash)throw Error('Comal Cloud requires an origin without a path or credentials.');if(url.protocol!=='https:'&&!(env.COMAL_ENV==='test'&&url.protocol==='http:'&&['localhost','127.0.0.1','[::1]','comal.localhost'].includes(url.hostname)))throw Error('Comal Cloud requires HTTPS.');return url.origin;}
|
|
12
12
|
export class CloudClient {
|
|
13
13
|
constructor({env=process.env,file,origin,fetcher=networkFetch}={}){this.env=env;this.file=file||env.COMAL_CLOUD_CONFIG||path.join(env.XDG_CONFIG_HOME||path.join(os.homedir(),'.config'),'comal','cloud','credentials.json');this.explicitOrigin=origin||env.COMAL_CLOUD_ORIGIN;this.fetcher=fetcher;}
|
|
14
|
-
async credentials(){try{const stat=await lstat(this.file);if(!stat.isFile()||stat.isSymbolicLink()||(stat.mode&0o077)!==0||process.getuid&&stat.uid!==process.getuid())throw Error('
|
|
15
|
-
async save(value){const directory=path.dirname(this.file);await mkdir(directory,{recursive:true,mode:0o700});const stat=await lstat(directory);if(!stat.isDirectory()||stat.isSymbolicLink()||(stat.mode&0o077)!==0)throw Error('
|
|
16
|
-
async request(origin,operation,input={},token,signal){const networkOrigin=this.env.COMAL_CLOUD_NETWORK_ORIGIN;if(networkOrigin&&this.env.COMAL_ENV!=='test')throw Error('
|
|
17
|
-
async invoke(operation,input={},options={}){const credential=await this.credentials();if(!credential)throw Error('
|
|
14
|
+
async credentials(){try{const stat=await lstat(this.file);if(!stat.isFile()||stat.isSymbolicLink()||(stat.mode&0o077)!==0||process.getuid&&stat.uid!==process.getuid())throw Error('Comal credentials must be a private file (0600) owned by your user.');const handle=await open(this.file,constants.O_RDONLY|constants.O_NOFOLLOW);let value;try{const current=await handle.stat();if(current.ino!==stat.ino||current.dev!==stat.dev||current.size>16384)throw Error('The credential file changed or is too large.');value=JSON.parse(await handle.readFile('utf8'));}finally{await handle.close();}const origin=cloudOrigin(value.origin,this.env);if(this.explicitOrigin&&cloudOrigin(this.explicitOrigin,this.env)!==origin)throw Error('The requested origin does not match the saved Comal session. Log in to that origin.');if(typeof value.token!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(value.token))throw Error('Invalid Comal credential. Log in again.');return{...value,origin};}catch(error){if(error.code==='ENOENT')return null;throw error;}}
|
|
15
|
+
async save(value){const directory=path.dirname(this.file);await mkdir(directory,{recursive:true,mode:0o700});const stat=await lstat(directory);if(!stat.isDirectory()||stat.isSymbolicLink()||(stat.mode&0o077)!==0)throw Error('The Comal credential directory must be private (0700).');const temp=path.join(directory,'.cloud-'+randomBytes(12).toString('hex'));try{await writeFile(temp,JSON.stringify(value)+'\n',{mode:0o600,flag:'wx'});await rename(temp,this.file);}finally{await unlink(temp).catch(error=>{if(error.code!=='ENOENT')throw error;});}}
|
|
16
|
+
async request(origin,operation,input={},token,signal){const networkOrigin=this.env.COMAL_CLOUD_NETWORK_ORIGIN;if(networkOrigin&&this.env.COMAL_ENV!=='test')throw Error('Comal laboratory transport is disabled in production.');const response=await this.fetcher(origin+'/api/operations/'+encodeURIComponent(operation),{networkOrigin,method:'POST',headers:{'Content-Type':'application/json',...(token?{Authorization:'Bearer '+token}:{})},body:JSON.stringify(input),signal:signal?AbortSignal.any([signal,AbortSignal.timeout(operation==='media.upload'?90000:30000)]):AbortSignal.timeout(operation==='media.upload'?90000:30000),maxBytes:2097152});if(response.headers.get('content-length')>2097152)throw Error('Comal response is too large.');const reader=response.body?.getReader();let length=0,parts=[];if(reader){try{for(;;){const{done,value}=await reader.read();if(done)break;length+=value.byteLength;if(length>2097152){await reader.cancel();throw Error('Comal response is too large.');}parts.push(Buffer.from(value));}}finally{reader.releaseLock();}}let body;try{body=JSON.parse(Buffer.concat(parts).toString('utf8'));}catch{throw Error('Comal returned an invalid response (HTTP '+response.status+').');}if(!response.ok||!body.ok){const error=Error((body.error?.code||'CLOUD_HTTP')+': '+(body.error?.message||'HTTP '+response.status));error.code=body.error?.code;error.status=response.status;throw error;}return body.result;}
|
|
17
|
+
async invoke(operation,input={},options={}){const credential=await this.credentials();if(!credential)throw Error('Connect your account with comal cloud login.');if(options.expectedAccount&&(credential.origin!==options.expectedAccount.origin||credential.user_id!==options.expectedAccount.user_id))throw Error('The Comal account changed during publication. Check the destination and publish again.');if(credential.expires_at<=Date.now()/1000)throw Error('Your Comal session expired. Use comal cloud login.');return this.request(credential.origin,operation,input,credential.token,options.signal);}
|
|
18
18
|
async status(options={}){const credential=await this.credentials();if(!credential)return{authenticated:false,origin:cloudOrigin(this.explicitOrigin||defaultOrigin,this.env)};const result=await this.invoke('account.inspect',{},options);const workspaces=await this.invoke('workspaces.list',{},options);return{authenticated:true,origin:credential.origin,user:result.user,identities:result.identities,expires_at:credential.expires_at,workspaces:workspaces.workspaces};}
|
|
19
|
-
async login({label=os.hostname()+' · Comal CLI',signal,onCode=()=>{}}={}){const origin=cloudOrigin(this.explicitOrigin||defaultOrigin,this.env);const proof=randomBytes(32).toString('base64url');const flow=await this.request(origin,'device.begin',{label:label.slice(0,80),code_challenge:createHash('sha256').update(proof).digest('base64url')},null,signal);if(!/^[A-Za-z0-9_-]{43}$/.test(flow.device_code)||!Number.isSafeInteger(flow.expires_in)||flow.expires_in<1||flow.expires_in>600)throw Error('
|
|
19
|
+
async login({label=os.hostname()+' · Comal CLI',signal,onCode=()=>{}}={}){const origin=cloudOrigin(this.explicitOrigin||defaultOrigin,this.env);const proof=randomBytes(32).toString('base64url');const flow=await this.request(origin,'device.begin',{label:label.slice(0,80),code_challenge:createHash('sha256').update(proof).digest('base64url')},null,signal);if(!/^[A-Za-z0-9_-]{43}$/.test(flow.device_code)||!Number.isSafeInteger(flow.expires_in)||flow.expires_in<1||flow.expires_in>600)throw Error('Invalid CLI connection contract.');const uri=new URL(flow.verification_uri_complete);if(uri.origin!==origin||uri.pathname!=='/connect')throw Error('Comal returned a different authentication destination.');await onCode({verification_uri:flow.verification_uri,verification_uri_complete:flow.verification_uri_complete,user_code:flow.user_code,expires_in:flow.expires_in});const deadline=Date.now()+flow.expires_in*1000;let interval=5;while(Date.now()<deadline){await delay(interval*1000,null,{signal});const result=await this.request(origin,'device.exchange',{device_code:flow.device_code,code_verifier:proof},null,signal);if(result.status==='authorization_pending')continue;if(result.status==='slow_down'){interval=Math.min(30,Math.max(interval+5,result.interval||10));continue;}if(result.status==='access_denied')throw Error('The connection was denied in Comal.');if(result.status!=='authenticated'||!/^[A-Za-z0-9_-]{43}$/.test(result.access_token)||!Number.isSafeInteger(result.expires_at))throw Error('Comal returned an invalid token exchange.');await this.save({version:1,origin,token:result.access_token,user_id:result.user_id,expires_at:result.expires_at});return this.status({signal});}throw Error('The Comal code expired. Start the connection again.');}
|
|
20
20
|
async logout({signal}={}){const credential=await this.credentials();if(!credential)return{revoked:false,local_session:false};try{await this.request(credential.origin,'account.logout',{},credential.token,signal);}catch(error){if(error.code!=='AUTH_INVALID')throw error;}await unlink(this.file);return{revoked:true,local_session:false,origin:credential.origin};}
|
|
21
21
|
}
|
|
22
22
|
const staticTypes=new Set(['html','htm','js','mjs','css','json','map','txt','svg','png','jpg','jpeg','webp','gif','ico','woff','woff2']);
|
|
23
|
-
export async function bundleArtifact(directory,{workspaceRoot}={}){const base=await realpath(directory);if(workspaceRoot){const workspace=await realpath(workspaceRoot);if(base!==workspace&&!base.startsWith(workspace+path.sep))throw Error('
|
|
24
|
-
|
|
23
|
+
export async function bundleArtifact(directory,{workspaceRoot,onVideo}={}){const base=await realpath(directory);if(workspaceRoot){const workspace=await realpath(workspaceRoot);if(base!==workspace&&!base.startsWith(workspace+path.sep))throw Error('Publication must stay within this session workspace.');}const files=[];let bytes=0,videos=0;async function visit(relative=''){const entries=await readdir(path.join(base,relative),{withFileTypes:true});entries.sort((a,b)=>a.name.localeCompare(b.name,'en'));for(const entry of entries){if(entry.name.startsWith('.'))continue;const name=relative?relative+'/'+entry.name:entry.name;const full=path.join(base,name);if(Buffer.byteLength(name)>512||/[\\%\x00-\x20\x7f?#]/.test(name)||name.split('/').some(part=>!part||part.startsWith('.')))throw Error('Published paths must be normalized relative paths without spaces or reserved characters: '+name);if(entry.isSymbolicLink())throw Error('Symbolic links cannot be published: '+name);if(entry.isDirectory()){await visit(name);continue;}if(entry.isFile()&&path.extname(entry.name).toLowerCase()==='.mp4'&&onVideo){if(++videos>4)throw Error('An artifact supports up to four videos.');await onVideo(full,name);continue;}if(!entry.isFile()||!staticTypes.has(path.extname(entry.name).slice(1).toLowerCase()))throw Error('The directory must contain only static web files: '+name);if(files.length>=256)throw Error('The artifact exceeds 256 files.');const current=await realpath(full);if(!current.startsWith(base+path.sep))throw Error('A file is outside the publication directory.');const handle=await open(full,constants.O_RDONLY|constants.O_NOFOLLOW);try{const stat=await handle.stat();if(!stat.isFile()||bytes+stat.size>8388608)throw Error('The artifact exceeds 8 MiB.');const content=await handle.readFile();bytes+=content.length;if(bytes>8388608)throw Error('The artifact exceeds 8 MiB.');files.push({path:name,content_base64:content.toString('base64'),sha256:hash(content)});}finally{await handle.close();}}}await visit();if(!files.length&&!videos)throw Error('The publication directory is empty.');return files;}
|
|
24
|
+
async function captureArtifactVideo(file,workspaceRoot){
|
|
25
|
+
const absolute=path.resolve(file),base=await realpath(workspaceRoot);const actual=await realpath(absolute);
|
|
26
|
+
if(!actual.startsWith(base+path.sep))throw Error('Video must stay inside the session workspace.');
|
|
27
|
+
if(path.extname(absolute).toLowerCase()!=='.mp4')throw Error('Only source MP4 videos are supported.');
|
|
28
|
+
const inspected=await lstat(actual,{bigint:true});
|
|
29
|
+
const handle=await open(actual,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
30
|
+
let content;
|
|
31
|
+
try{const before=await handle.stat({bigint:true});if(before.ino!==inspected.ino||before.dev!==inspected.dev||!before.isFile()||before.size<16n||before.size>8388608n)throw Error('A source MP4 of at most 8 MiB is required.');
|
|
32
|
+
const buffer=Buffer.alloc(Number(before.size)+1);let length=0;while(length<buffer.length){const read=await handle.read(buffer,length,buffer.length-length,null);if(!read.bytesRead)break;length+=read.bytesRead;}
|
|
33
|
+
const after=await handle.stat({bigint:true});if(length!==Number(before.size)||before.ino!==after.ino||before.dev!==after.dev||before.mtimeNs!==after.mtimeNs||before.ctimeNs!==after.ctimeNs)throw Error('Video changed while being captured.');
|
|
34
|
+
if(await realpath(absolute)!==actual)throw Error('Video path changed during capture.');const current=await lstat(actual,{bigint:true});if(current.ino!==before.ino||current.dev!==before.dev)throw Error('Video path changed during capture.');
|
|
35
|
+
content=buffer.subarray(0,length);if(content.subarray(4,8).toString()!=='ftyp')throw Error('The selected file is not an MP4.');
|
|
36
|
+
}finally{await handle.close();}
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
async function sendArtifactVideo(client,content,{workspace_id,session_id},options={}){
|
|
40
|
+
const account=await client.credentials();if(!account)throw Error('Connect your account with /cloud login.');
|
|
41
|
+
if(options.expectedAccount&&(options.expectedAccount.origin!==account.origin||options.expectedAccount.user_id!==account.user_id))throw Error('The Comal account changed before video upload.');
|
|
42
|
+
const authority={...options,expectedAccount:{origin:account.origin,user_id:account.user_id}};
|
|
43
|
+
let result=await client.invoke('media.upload',{workspace_id,session_id,sha256:hash(content),content_base64:content.toString('base64')},authority);
|
|
44
|
+
const deadline=Date.now()+Math.max(0,Math.min(90000,options.mediaWaitMs??90000));
|
|
45
|
+
while(!['ready','failed'].includes(result.status)&&Date.now()<deadline){await delay(Math.min(2000,deadline-Date.now()),null,{signal:options.signal});result=await client.invoke('media.inspect',{media_id:result.id},authority);}
|
|
46
|
+
// Acknowledged asynchronous processing is a domain result, not an uncertain
|
|
47
|
+
// invocation failure. Only a ready media binding can reach artifacts.publish.
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export async function uploadArtifactVideo(client,{file,workspaceRoot,...metadata},options={}){return sendArtifactVideo(client,await captureArtifactVideo(file,workspaceRoot),metadata,options);}
|
|
51
|
+
export async function publishArtifact(client,{directory,workspaceRoot=directory,...metadata},options={}){
|
|
52
|
+
const account=await client.credentials();if(!account)throw Error('Connect your account with comal cloud login.');
|
|
53
|
+
const authority={...options,expectedAccount:{origin:account.origin,user_id:account.user_id}},media=[],videos=[];
|
|
54
|
+
const files=await bundleArtifact(directory,{workspaceRoot,onVideo:async(file,name)=>{
|
|
55
|
+
videos.push({content:await captureArtifactVideo(file,workspaceRoot),path:name.slice(0,-4)+'.player.html'});
|
|
56
|
+
}});
|
|
57
|
+
if(metadata.entrypoint?.toLowerCase().endsWith('.mp4'))metadata.entrypoint=metadata.entrypoint.slice(0,-4)+'.player.html';
|
|
58
|
+
if(videos.length){
|
|
59
|
+
const paths=new Set(files.map(file=>file.path));for(const video of videos){if(paths.has(video.path))throw Error('Generated video player conflicts with another published path: '+video.path);paths.add(video.path);}
|
|
60
|
+
if(paths.size>256||files.reduce((sum,file)=>sum+Buffer.byteLength(file.content_base64,'base64'),0)+videos.length*2048>8388608)throw Error('The artifact exceeds its static budget including generated video players.');
|
|
61
|
+
if(!paths.has(metadata.entrypoint)||!/[.]html?$/i.test(metadata.entrypoint))throw Error('The entrypoint must exist as HTML or a selected MP4; use clip.mp4 for its generated player.');
|
|
62
|
+
}
|
|
63
|
+
for(const capture of videos){const video=await sendArtifactVideo(client,capture.content,metadata,authority);if(video.status!=='ready')return{status:video.status==='failed'?'media_failed':'media_processing',candidate_created:false,media:{id:video.id,status:video.status,source_sha256:hash(capture.content)},next_action:video.status==='failed'?'The acknowledged media job failed. Preserve its ID; inspect the source and hosting before a corrected publication. No artifact candidate was created.':'Media upload was acknowledged and is still processing. Retry ArtifactPublish with the SAME directory and metadata to reconcile the source binding; the backend deduplicates the upload. No artifact ID exists yet; do not verify or release.'};media.push({path:capture.path,media_id:video.id});}
|
|
64
|
+
const input={...metadata,files,...(media.length?{media}:{})};
|
|
65
|
+
input.request_id='comal_'+hash(JSON.stringify({origin:account.origin,user:account.user_id,input}));
|
|
66
|
+
try{return await client.invoke('artifacts.publish',input,authority);}
|
|
67
|
+
catch(error){
|
|
68
|
+
// This authenticated backend precondition rejects changing the accepted
|
|
69
|
+
// checks before any candidate mutation. It is a domain refusal, not a lost
|
|
70
|
+
// transport outcome. Earlier acknowledged media stays source-deduplicated.
|
|
71
|
+
if(error.code==='CONTRACT_CHANGE'&&error.status===409)return{status:'refused',code:'CONTRACT_CHANGE',candidate_created:false,accepted_checks_preserved:true,next_action:'The backend rejected changing the accepted verification contract. Inspect the current artifact and correct the app to satisfy its ORIGINAL checks. Re-publish the corrected files with the accepted checks and observed base_version; a different contract requires an explicit user decision.'};
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { getQuickJS } from 'quickjs-emscripten';
|
|
3
|
+
|
|
4
|
+
const { code, catalogue, limits } = workerData;
|
|
5
|
+
const module = await getQuickJS(), runtime = module.newRuntime();
|
|
6
|
+
runtime.setMemoryLimit(limits.memoryBytes); runtime.setMaxStackSize(limits.stackBytes);
|
|
7
|
+
let cpuLeft = limits.cpuMs, activeStart = performance.now(), stopped = false, sequence = 0, emitted = 2;
|
|
8
|
+
runtime.setInterruptHandler(() => performance.now() - activeStart > cpuLeft);
|
|
9
|
+
const vm = runtime.newContext(), pending = new Map(), output = [];
|
|
10
|
+
const allowedNames = new Set(catalogue.map(row => row.name));
|
|
11
|
+
const enter = fn => { activeStart = performance.now(); try { return fn(); } finally { cpuLeft -= performance.now() - activeStart; } };
|
|
12
|
+
const expose = (name, fn) => { const handle = vm.newFunction(name, fn); vm.setProp(vm.global, name, handle); handle.dispose(); };
|
|
13
|
+
const jsonHandle = value => vm.unwrapResult(enter(() => vm.evalCode(`JSON.parse(${JSON.stringify(JSON.stringify(value))})`)));
|
|
14
|
+
function fail(error) {
|
|
15
|
+
if (stopped) return; stopped = true;
|
|
16
|
+
const message = error.message || String(error);
|
|
17
|
+
const code = error.code || (cpuLeft <= 0 || /interrupted/.test(message) ? 'CODE_RUN_CPU_LIMIT' : /out of memory|allocation failed/i.test(message) ? 'CODE_RUN_MEMORY_LIMIT' : /stack overflow/i.test(message) ? 'CODE_RUN_STACK_LIMIT' : /output byte limit/.test(message) ? 'CODE_RUN_OUTPUT_LIMIT' : /call\/concurrency limit/.test(message) ? 'CODE_RUN_CALL_LIMIT' : /argument byte limit/.test(message) ? 'CODE_RUN_ARGUMENT_LIMIT' : /outside the read-only caller catalogue/.test(message) ? 'CODE_RUN_TOOL_UNAVAILABLE' : 'CODE_RUN_SCRIPT');
|
|
18
|
+
parentPort.postMessage({ type: 'error', code, message, output });
|
|
19
|
+
}
|
|
20
|
+
expose('__emit', value => {
|
|
21
|
+
const text = vm.getString(value); emitted += Buffer.byteLength(text) + (output.length ? 1 : 0);
|
|
22
|
+
if (emitted > limits.outputBytes) throw Object.assign(new Error('CodeRun output byte limit exceeded.'), {code:'CODE_RUN_OUTPUT_LIMIT'});
|
|
23
|
+
const item = JSON.parse(text); output.push(item); parentPort.postMessage({type:'output',value:item});
|
|
24
|
+
});
|
|
25
|
+
expose('__call', (nameHandle, argsHandle) => {
|
|
26
|
+
const name = vm.getString(nameHandle), raw = vm.getString(argsHandle);
|
|
27
|
+
// Enforce before IPC too: a VM loop cannot flood the parent with queued RPCs.
|
|
28
|
+
if (++sequence > limits.calls || pending.size >= limits.concurrency) throw new Error('CodeRun tool call/concurrency limit exceeded.');
|
|
29
|
+
if (!allowedNames.has(name)) throw new Error('Tool is outside the read-only caller catalogue.');
|
|
30
|
+
if (Buffer.byteLength(raw) > limits.argumentBytes) throw new Error('CodeRun tool argument byte limit exceeded.');
|
|
31
|
+
const deferred = vm.newPromise(), id = sequence; pending.set(id, deferred);
|
|
32
|
+
parentPort.postMessage({ type: 'call', id, name, args: JSON.parse(raw) });
|
|
33
|
+
return deferred.handle;
|
|
34
|
+
});
|
|
35
|
+
let script;
|
|
36
|
+
try {
|
|
37
|
+
// All metadata enters as JSON. Bindings expose only VM values, never native
|
|
38
|
+
// host objects or host function constructors. No module loader is installed.
|
|
39
|
+
vm.unwrapResult(enter(() => vm.evalCode(`
|
|
40
|
+
const ALL_TOOLS = Object.freeze(${JSON.stringify(catalogue)}.map(t => Object.freeze(t)));
|
|
41
|
+
const __allowed = new Set(ALL_TOOLS.map(t => t.name));
|
|
42
|
+
const tools = Object.freeze({...Object.fromEntries(ALL_TOOLS.map(t => [t.name, args => __call(t.name, JSON.stringify(args ?? {})).then(JSON.parse)])), call: (name,args) => __call(String(name),JSON.stringify(args ?? {})).then(JSON.parse)});
|
|
43
|
+
function text(value) { const encoded = JSON.stringify(value); __emit(encoded === undefined ? 'null' : encoded); }
|
|
44
|
+
function searchTools(query = '') { return ALL_TOOLS.filter(t => (t.name+' '+t.description).toLowerCase().includes(String(query).toLowerCase())).map(t => ({name:t.name,description:t.description})); }
|
|
45
|
+
function describeTool(name) { return ALL_TOOLS.find(t => t.name === name) || null; }
|
|
46
|
+
Object.freeze(tools); Object.freeze(searchTools); Object.freeze(describeTool);
|
|
47
|
+
`))).dispose();
|
|
48
|
+
script = vm.unwrapResult(enter(() => vm.evalCode(`(async () => { "use strict";\n${code}\n})().then(value => { if(value !== undefined) text(value); })`, 'comal-code-run.js')));
|
|
49
|
+
pump();
|
|
50
|
+
} catch (error) { fail(error); }
|
|
51
|
+
function pump() {
|
|
52
|
+
if (stopped) return;
|
|
53
|
+
try {
|
|
54
|
+
const jobs = enter(() => runtime.executePendingJobs());
|
|
55
|
+
if (jobs.error) { const error = vm.dump(jobs.error); jobs.error.dispose(); throw error; }
|
|
56
|
+
const state = vm.getPromiseState(script);
|
|
57
|
+
if (state.type === 'fulfilled') { state.value.dispose(); stopped = true; parentPort.postMessage({type:'done', output}); }
|
|
58
|
+
else if (state.type === 'rejected') { const error = vm.dump(state.error); state.error.dispose(); throw error; }
|
|
59
|
+
else if (!pending.size) throw new Error('CodeRun promise cannot make progress without an awaited tool.');
|
|
60
|
+
} catch (error) { fail(error); }
|
|
61
|
+
}
|
|
62
|
+
parentPort.on('message', message => {
|
|
63
|
+
if (stopped || message.type !== 'result') return;
|
|
64
|
+
const deferred = pending.get(message.id); if (!deferred) return; pending.delete(message.id);
|
|
65
|
+
try {
|
|
66
|
+
const handle = message.ok ? vm.newString(message.value) : jsonHandle({name:'Error', message:message.value, code:message.code || 'CODE_RUN_TOOL'});
|
|
67
|
+
if (message.ok) deferred.resolve(handle); else deferred.reject(handle);
|
|
68
|
+
handle.dispose(); deferred.dispose(); pump();
|
|
69
|
+
} catch (error) { fail(error); }
|
|
70
|
+
});
|
package/src/code-run.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { digest } from './operations.mjs';
|
|
4
|
+
import { chromeLabObservation } from './read-only-check.mjs';
|
|
5
|
+
|
|
6
|
+
export const CODE_RUN_LIMITS = Object.freeze({ sourceBytes: 32768, outputBytes: 16384, resultBytes: 1048576, argumentBytes: 65536, calls: 32, concurrency: 4, wallMs: 30000, cpuMs: 2000, memoryBytes: 33554432, stackBytes: 262144, cleanupMs: 2000 });
|
|
7
|
+
const ceiling = { ...CODE_RUN_LIMITS, wallMs: 60000, cpuMs: 5000, memoryBytes: 67108864 };
|
|
8
|
+
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
9
|
+
const activeRuns = new Set();
|
|
10
|
+
export const CODE_RUN_MAX_WORKERS = 2;
|
|
11
|
+
export const codeRunEligible = row => Boolean(row && row.name !== 'CodeRun' && row.permission === row.name && !row.steps && row.audience !== 'human' && ['none', 'read'].includes(row.effects?.workspace) && ['none', 'read'].includes(row.effects?.external) && (row.effects?.process === 'none' || row.effects?.process === 'spawn' && row.origin?.plugin === 'chrome-lab' && chromeLabObservation(row.name)) && row.effects?.authority === 'none');
|
|
12
|
+
export const codeRunDefinition = Object.freeze({
|
|
13
|
+
name: 'CodeRun', description: 'Experimental read-only JavaScript composition in an isolated QuickJS VM. Use searchTools/describeTool for the currently visible tool catalogue, tools.call(name,args) or tools[name](args) to call tools and text(value) or return value to emit selected observations. No filesystem, network, Node APIs, timers, persistence or nested CodeRun. Every tool call retains its own House judgment and receipt. Effects completed before an error remain recorded; never automatically replay the script.',
|
|
14
|
+
input_schema: { type: 'object', properties: { code: { type: 'string', minLength: 1, maxLength: 32768 } }, required: ['code'], additionalProperties: false },
|
|
15
|
+
effects: { workspace: 'read', external: 'read', process: 'spawn', authority: 'none' }, steps: true, permission: 'CodeRun', run: runCode,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Caller supplies a captured, actor-scoped catalogue AND its matching House invoke.
|
|
19
|
+
// No tool code, host objects, credentials or runtime resources cross the VM boundary.
|
|
20
|
+
export async function runCode({ code }, options = {}) {
|
|
21
|
+
const { invoke, recordWork, signal } = options;
|
|
22
|
+
if (typeof invoke !== 'function' || typeof recordWork !== 'function' || !Array.isArray(options.catalogue)) throw failure('CODE_RUN_HOST', 'CodeRun requires the caller catalogue, governed invoke and durable recorder.');
|
|
23
|
+
const limits = { ...CODE_RUN_LIMITS };
|
|
24
|
+
for (const [key, value] of Object.entries(options.limits || {})) {
|
|
25
|
+
if (!(key in limits) || !Number.isSafeInteger(value) || value < 1 || value > ceiling[key]) throw failure('CODE_RUN_LIMIT', `Invalid CodeRun limit: ${key}.`);
|
|
26
|
+
limits[key] = value;
|
|
27
|
+
}
|
|
28
|
+
if (typeof code !== 'string' || !code.trim() || Buffer.byteLength(code) > limits.sourceBytes) throw failure('CODE_RUN_SOURCE_LIMIT', 'CodeRun source exceeds its byte limit or is empty.');
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
const catalogue = options.catalogue.filter(codeRunEligible).map(row => ({ name: row.name, description: row.description, input_schema: row.input_schema, effects: row.effects, contract_digest: row.contract_digest }));
|
|
31
|
+
const visible = new Map(catalogue.map(row => [row.name, row]));
|
|
32
|
+
const runId = randomUUID(), sourceRef = digest(code), controller = new AbortController();
|
|
33
|
+
const pending = new Map(), calls = []; let total = 0, closed = false, worker, timer;
|
|
34
|
+
if (activeRuns.size >= CODE_RUN_MAX_WORKERS) throw failure('CODE_RUN_BUSY', 'Two CodeRun runtimes are already active; wait for completion before proposing another run.');
|
|
35
|
+
activeRuns.add(runId);
|
|
36
|
+
try { await recordWork({ type: 'code_run_source', run_id: runId, source_ref: sourceRef, source: code, limits, max_workers: CODE_RUN_MAX_WORKERS, catalogue: catalogue.map(({name,contract_digest}) => ({name,contract_digest})) }); }
|
|
37
|
+
catch(error) { activeRuns.delete(runId); throw error; }
|
|
38
|
+
let rejectRun;
|
|
39
|
+
const abort = () => { const error = signal?.reason instanceof Error ? signal.reason : failure('CODE_RUN_ABORTED', 'CodeRun was cancelled.'); controller.abort(error); rejectRun?.(error); };
|
|
40
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
41
|
+
let selected = [], runError;
|
|
42
|
+
try {
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
worker = new Worker(new URL('./code-run-worker.mjs', import.meta.url), { workerData: { code, catalogue, limits }, resourceLimits: { maxOldGenerationSizeMb: 96, stackSizeMb: 4 }, execArgv: [] });
|
|
45
|
+
selected = await new Promise((resolve, reject) => {
|
|
46
|
+
rejectRun = reject;
|
|
47
|
+
timer = setTimeout(() => { const e = failure('CODE_RUN_TIMEOUT', 'CodeRun wall deadline exceeded.'); controller.abort(e); reject(e); }, limits.wallMs);
|
|
48
|
+
worker.on('error', reject);
|
|
49
|
+
worker.on('exit', exit => { if (!closed && exit !== 0) reject(failure('CODE_RUN_WORKER', `CodeRun worker exited (${exit}).`)); });
|
|
50
|
+
worker.on('message', message => {
|
|
51
|
+
if (closed) return;
|
|
52
|
+
if (message.type === 'output') { selected.push(message.value); return; }
|
|
53
|
+
if (message.type === 'done') { resolve(message.output); return; }
|
|
54
|
+
if (message.type === 'error') { selected = message.output || []; reject(failure(message.code || 'CODE_RUN_SCRIPT', message.message)); return; }
|
|
55
|
+
if (message.type !== 'call') return;
|
|
56
|
+
const { id, name, args } = message;
|
|
57
|
+
const answer = (ok, value, errorCode) => { if (!closed) worker.postMessage({ type: 'result', id, ok, value, code: errorCode }); };
|
|
58
|
+
if (controller.signal.aborted) { answer(false, 'CodeRun is stopping.', 'CODE_RUN_ABORTED'); return; }
|
|
59
|
+
if (!visible.has(name)) { answer(false, 'Tool is outside the read-only caller catalogue.', 'CODE_RUN_TOOL_UNAVAILABLE'); return; }
|
|
60
|
+
if (++total > limits.calls || pending.size >= limits.concurrency) { answer(false, 'CodeRun tool call/concurrency limit exceeded.', 'CODE_RUN_CALL_LIMIT'); return; }
|
|
61
|
+
if (Buffer.byteLength(JSON.stringify(args) ?? '') > limits.argumentBytes) { answer(false, 'CodeRun tool arguments exceed their byte limit.', 'CODE_RUN_ARGUMENT_LIMIT'); return; }
|
|
62
|
+
const callId = `${runId}:${id}`, row = { call_id: callId, operation: name, contract_digest: visible.get(name).contract_digest, arguments_digest: digest(args), status: 'started' }; calls.push(row);
|
|
63
|
+
const work = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
// This callback runs in the parent operation's async context; House
|
|
66
|
+
// revalidates contracts, visibility and authority at the effect frontier.
|
|
67
|
+
const result = await invoke(name, args, { signal: controller.signal, callId, modelProposed: true });
|
|
68
|
+
row.status = 'returned'; row.output_ref = digest(result);
|
|
69
|
+
const encoded = JSON.stringify(result);
|
|
70
|
+
if (Buffer.byteLength(encoded ?? '') > limits.resultBytes) throw failure('CODE_RUN_RESULT_LIMIT', 'Tool observation exceeds the VM transfer limit; the complete House receipt remains recorded.');
|
|
71
|
+
answer(true, encoded ?? 'null');
|
|
72
|
+
} catch (error) { row.status = row.status === 'returned' ? 'returned_untransferred' : 'failed_or_refused'; row.code = error.code; answer(false, error.message, error.code); }
|
|
73
|
+
})();
|
|
74
|
+
pending.set(id, work); work.finally(() => pending.delete(id));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
} catch (error) { runError = error; }
|
|
78
|
+
finally {
|
|
79
|
+
closed = true; clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
80
|
+
controller.abort(runError || failure('CODE_RUN_FINISHED', 'CodeRun script finished; unawaited calls are cancelled.'));
|
|
81
|
+
await worker?.terminate().catch(() => {});
|
|
82
|
+
if (pending.size) {
|
|
83
|
+
let cleanupTimer;
|
|
84
|
+
const settled = await Promise.race([Promise.allSettled([...pending.values()]).then(() => true), new Promise(resolve => { cleanupTimer = setTimeout(() => resolve(false), limits.cleanupMs); })]); clearTimeout(cleanupTimer);
|
|
85
|
+
if (!settled) runError = failure('CODE_RUN_CLEANUP_PENDING', 'A host tool ignored cancellation; its outcome remains unknown/partial. Pending call IDs are recorded. Do not replay this script.');
|
|
86
|
+
}
|
|
87
|
+
activeRuns.delete(runId);
|
|
88
|
+
}
|
|
89
|
+
const outputRef = digest(selected);
|
|
90
|
+
const outcome = { run_id: runId, source_ref: sourceRef, output_ref: outputRef, output: selected, status: runError ? 'failed' : 'returned', code: runError?.code, error: runError?.message, calls: structuredClone(calls), pending_calls: [...pending.keys()].map(id => `${runId}:${id}`), replayed: false };
|
|
91
|
+
await recordWork({ type: 'code_run_result', ...outcome });
|
|
92
|
+
if (runError) {
|
|
93
|
+
const details = { ...outcome, message: runError.message, next_action: 'Inspect this run and individual House receipts using the recorded call IDs. Preserve completed observations. Do not replay the script automatically; pending host calls may still have unknown/partial outcomes.' };
|
|
94
|
+
const error = failure(runError.code || 'CODE_RUN_SCRIPT', JSON.stringify(details)); error.run_id = runId; error.details = details; throw error;
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify({ run_id: runId, output: selected, source_ref: sourceRef, output_ref: outputRef, calls: calls.length });
|
|
97
|
+
}
|
package/src/codex-provider.mjs
CHANGED
|
@@ -19,16 +19,16 @@ function responseFailure(value, httpStatus) {
|
|
|
19
19
|
const field = (value, pattern = /^[A-Za-z0-9_.:-]{1,100}$/) => typeof value === 'string' && pattern.test(value) ? value : undefined;
|
|
20
20
|
const metadata = Object.fromEntries(Object.entries({ code: field(upstream?.code), type: field(upstream?.type), param: field(upstream?.param, /^[A-Za-z0-9_.\[\]-]{1,160}$/) }).filter(([, value]) => value !== undefined));
|
|
21
21
|
const reason = field(response?.incomplete_details?.reason);
|
|
22
|
-
let code = 'CODEX_RESPONSE_INCOMPLETE', message = '
|
|
22
|
+
let code = 'CODEX_RESPONSE_INCOMPLETE', message = 'Incomplete Codex response';
|
|
23
23
|
if (reason === 'max_output_tokens') code = 'OUTPUT_LENGTH';
|
|
24
24
|
else if (metadata.code === 'context_length_exceeded') code = 'CONTEXT_LENGTH';
|
|
25
|
-
else if (metadata.code === 'invalid_function_parameters') { code = 'CODEX_TOOL_SCHEMA_INVALID'; message = 'Codex
|
|
26
|
-
else if (metadata.type === 'invalid_request_error' || metadata.code === 'invalid_request_error') { code = 'CODEX_REQUEST_REJECTED'; message = 'Codex
|
|
27
|
-
else if (upstream) { code = 'CODEX_RESPONSE_FAILED'; message = 'Codex
|
|
28
|
-
if (httpStatus === 401) { code = 'PROVIDER_AUTH_REQUIRED'; message = 'Codex
|
|
25
|
+
else if (metadata.code === 'invalid_function_parameters') { code = 'CODEX_TOOL_SCHEMA_INVALID'; message = 'Codex rejected the tool schema'; }
|
|
26
|
+
else if (metadata.type === 'invalid_request_error' || metadata.code === 'invalid_request_error') { code = 'CODEX_REQUEST_REJECTED'; message = 'Codex rejected the request'; }
|
|
27
|
+
else if (upstream) { code = 'CODEX_RESPONSE_FAILED'; message = 'Codex returned an error'; }
|
|
28
|
+
if (httpStatus === 401) { code = 'PROVIDER_AUTH_REQUIRED'; message = 'Codex requires login'; }
|
|
29
29
|
else if (httpStatus && code === 'CODEX_RESPONSE_INCOMPLETE') { code = 'CODEX_HTTP_ERROR'; message = `Codex HTTP ${httpStatus}`; }
|
|
30
30
|
const diagnostic = [metadata.code, metadata.param, reason].filter(Boolean).join('; ');
|
|
31
|
-
return Object.assign(fail(`${message}${diagnostic ? ` (${diagnostic})` : ''}; no
|
|
31
|
+
return Object.assign(fail(`${message}${diagnostic ? ` (${diagnostic})` : ''}; no tools from this response were executed.`, code), {
|
|
32
32
|
usage_accounting:normalizeUsage('codex',response?.usage),
|
|
33
33
|
details: { provider: 'codex', upstream: metadata, ...(reason ? { reason } : {}), ...(httpStatus ? { http_status: httpStatus } : {}), tools_executed: false,
|
|
34
34
|
...(['CODEX_TOOL_SCHEMA_INVALID', 'CODEX_REQUEST_REJECTED'].includes(code) ? { retryable: false } : {}) }
|
|
@@ -38,17 +38,17 @@ function responseFailure(value, httpStatus) {
|
|
|
38
38
|
// Some Codex responses omit Content-Type despite carrying SSE. Inspect a
|
|
39
39
|
// bounded prefix, then replay every byte: framing must not depend on a header.
|
|
40
40
|
async function responseFormat(body) {
|
|
41
|
-
if (!body) throw fail('Codex
|
|
41
|
+
if (!body) throw fail('Codex returned an empty body.', 'CODEX_RESPONSE_INCOMPLETE');
|
|
42
42
|
const reader=body.getReader(),chunks=[],decoder=new TextDecoder();let prefix='',size=0,format;
|
|
43
43
|
try {
|
|
44
44
|
while(!format){
|
|
45
45
|
const{done,value}=await reader.read();
|
|
46
|
-
if(done)throw fail('Codex
|
|
46
|
+
if(done)throw fail('Codex returned an empty or incomplete body.', 'CODEX_RESPONSE_INCOMPLETE');
|
|
47
47
|
chunks.push(value);const head=value.subarray(0,Math.max(0,4096-size));size+=head.byteLength;
|
|
48
48
|
prefix+=decoder.decode(head,{stream:true});const start=prefix.trimStart();
|
|
49
49
|
if(start.startsWith('{'))format='json';
|
|
50
50
|
else if(start.startsWith(':') || /^(?:event|data|id|retry):/.test(start))format='sse';
|
|
51
|
-
else if(size>=4096 || start && !['event:','data:','id:','retry:'].some(field=>field.startsWith(start)))throw fail('
|
|
51
|
+
else if(size>=4096 || start && !['event:','data:','id:','retry:'].some(field=>field.startsWith(start)))throw fail('Unknown Codex response format; no tools were executed.');
|
|
52
52
|
}
|
|
53
53
|
}catch(error){await reader.cancel().catch(()=>{});reader.releaseLock();throw error;}
|
|
54
54
|
return{format,body:{async *[Symbol.asyncIterator](){
|
|
@@ -59,8 +59,8 @@ async function responseFormat(body) {
|
|
|
59
59
|
|
|
60
60
|
async function responseJSON(body, maxBytes = 8 * 1024 * 1024) {
|
|
61
61
|
const decoder=new TextDecoder();let text='',size=0;
|
|
62
|
-
for await(const chunk of body){size+=chunk.byteLength;if(size>maxBytes)throw fail('
|
|
63
|
-
try{return JSON.parse(text+decoder.decode());}catch{throw fail('Codex
|
|
62
|
+
for await(const chunk of body){size+=chunk.byteLength;if(size>maxBytes)throw fail('The Codex response exceeded its byte limit.');text+=decoder.decode(chunk,{stream:true});}
|
|
63
|
+
try{return JSON.parse(text+decoder.decode());}catch{throw fail('Codex returned invalid JSON; no tools were executed.');}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function toCodexInput(messages) {
|
|
@@ -79,7 +79,7 @@ export function toCodexInput(messages) {
|
|
|
79
79
|
const content = blocks.flatMap(block => {
|
|
80
80
|
if (block.type === 'text') return [{ type: 'input_text', text: block.text }];
|
|
81
81
|
if (block.type === 'image' && block.source?.type === 'base64') return [{ type: 'input_image', image_url: `data:${block.source.media_type};base64,${block.source.data}` }];
|
|
82
|
-
if (['video', 'media', 'image_reference'].includes(block.type)) throw fail('
|
|
82
|
+
if (['video', 'media', 'image_reference'].includes(block.type)) throw fail('Unsupported media or unresolved reference.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
83
83
|
return [];
|
|
84
84
|
});
|
|
85
85
|
if (content.length) input.push({ role: 'user', content });
|
|
@@ -98,13 +98,13 @@ function normalized(response) {
|
|
|
98
98
|
else if (block.type === 'refusal') content.push({ type: 'text', text: block.refusal });
|
|
99
99
|
}
|
|
100
100
|
} else if (item.type === 'function_call') {
|
|
101
|
-
if (!id(item.call_id) || ids.has(item.call_id) || !id(item.name) || typeof item.arguments !== 'string') throw fail('
|
|
101
|
+
if (!id(item.call_id) || ids.has(item.call_id) || !id(item.name) || typeof item.arguments !== 'string') throw fail('Invalid Codex tool call.');
|
|
102
102
|
let args; try { args = JSON.parse(item.arguments); } catch { throw fail('Argumentos Codex incompletos.', 'TOOL_ARGUMENTS_INVALID'); }
|
|
103
103
|
if (!args || typeof args !== 'object' || Array.isArray(args)) throw fail('Argumentos Codex deben ser objeto.', 'TOOL_ARGUMENTS_INVALID');
|
|
104
104
|
ids.add(item.call_id); content.push({ type: 'tool_use', id: item.call_id, name: item.name, input: args });
|
|
105
105
|
} else if (item.type === 'reasoning') content.push({ type: 'openai_reasoning', text: (item.summary || []).map(block => block.text || '').join('\n'), codex_item: structuredClone(item) });
|
|
106
106
|
}
|
|
107
|
-
if (!content.length) throw fail('Codex
|
|
107
|
+
if (!content.length) throw fail('Codex returned an empty response.');
|
|
108
108
|
return { role: 'assistant', content, stop_reason: ids.size ? 'tool_use' : 'end_turn', usage_accounting:normalizeUsage('codex',response.usage), usage: {
|
|
109
109
|
input_tokens: response.usage?.input_tokens || 0, output_tokens: response.usage?.output_tokens || 0,
|
|
110
110
|
...(response.usage?.input_tokens_details?.cached_tokens ? { cache_read_input_tokens: response.usage.input_tokens_details.cached_tokens } : {}) } };
|
|
@@ -113,14 +113,14 @@ function normalized(response) {
|
|
|
113
113
|
// Subscription bearer tokens go only to the fixed Codex backend, never api.openai.com.
|
|
114
114
|
export class CodexProvider {
|
|
115
115
|
constructor({ authManager, authProvider = 'codex', model, maxTokens = 4096, effort = 'auto', fetchImpl = globalThis.fetch, timeoutMs, idleTimeoutMs, vision, capabilitiesConfigured, profile, ...options } = {}) {
|
|
116
|
-
if (!authManager || authProvider !== 'codex') throw new Error('CodexProvider
|
|
116
|
+
if (!authManager || authProvider !== 'codex') throw new Error('CodexProvider requires AuthManager and authProvider codex.');
|
|
117
117
|
this.authManager = authManager; this.authProvider = authProvider; this.protocol = 'openai'; this.model = model;
|
|
118
118
|
this.baseURL = 'https://chatgpt.com/backend-api/codex'; this.fetch = fetchImpl; this.maxTokens = maxTokens;
|
|
119
119
|
this.capabilitiesConfigured = { ...profile?.capabilitiesConfigured, ...capabilitiesConfigured };
|
|
120
|
-
for (const [name, enabled] of Object.entries(this.capabilitiesConfigured)) if (!['text', 'tools', 'thinking', 'vision', 'video', 'web_search'].includes(name) || typeof enabled !== 'boolean') throw fail('
|
|
121
|
-
if (this.capabilitiesConfigured.text === false || this.capabilitiesConfigured.video === true || this.capabilitiesConfigured.web_search === true) throw fail('
|
|
120
|
+
for (const [name, enabled] of Object.entries(this.capabilitiesConfigured)) if (!['text', 'tools', 'thinking', 'vision', 'video', 'web_search'].includes(name) || typeof enabled !== 'boolean') throw fail('Invalid Codex capability configuration.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
121
|
+
if (this.capabilitiesConfigured.text === false || this.capabilitiesConfigured.video === true || this.capabilitiesConfigured.web_search === true) throw fail('The Codex adapter requires text and does not implement native video/web_search.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
122
122
|
this.effort = normalizeEffort(effort); this.vision = vision || (Object.hasOwn(this.capabilitiesConfigured, 'vision') ? this.capabilitiesConfigured.vision ? 'on' : 'off' : 'auto'); this.imageTokens = options.imageTokens || 4096;
|
|
123
|
-
if (!['auto', 'on', 'off'].includes(this.vision)) throw fail('
|
|
123
|
+
if (!['auto', 'on', 'off'].includes(this.vision)) throw fail('Invalid Codex vision setting.');
|
|
124
124
|
this.configuredCapabilities = this.capabilitiesConfigured;
|
|
125
125
|
if (profile?.name) this.profileName = profile.name;
|
|
126
126
|
Object.assign(this, modelTimingSettings({ timeoutMs, idleTimeoutMs }));
|
|
@@ -139,10 +139,10 @@ export class CodexProvider {
|
|
|
139
139
|
async listModels({ signal } = {}) {
|
|
140
140
|
const timeout = AbortSignal.timeout(15000), response = await this._fetch(`models?client_version=${CODEX_CATALOG_VERSION}`, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
141
141
|
const text = await response.text();
|
|
142
|
-
if (text.length > 1024 * 1024) throw fail('
|
|
143
|
-
let data; try { data = JSON.parse(text); } catch { throw fail('
|
|
142
|
+
if (text.length > 1024 * 1024) throw fail('Codex catalog is too large.');
|
|
143
|
+
let data; try { data = JSON.parse(text); } catch { throw fail('Invalid Codex catalog.'); }
|
|
144
144
|
const models = data.models || data.data;
|
|
145
|
-
if (!Array.isArray(models)) throw fail('Codex
|
|
145
|
+
if (!Array.isArray(models)) throw fail('Codex did not advertise models.', 'MODEL_DISCOVERY_UNAVAILABLE');
|
|
146
146
|
this.discoveredModels = models.filter(item => id(item.slug || item.id)).slice(0, 1024);
|
|
147
147
|
return this.discoveredModels.filter(item => item.visibility !== 'hide').map(item => ({ id: item.slug || item.id,
|
|
148
148
|
...(typeof item.display_name === 'string' ? { display_name: item.display_name.slice(0, 256) } : {}),
|
|
@@ -152,7 +152,7 @@ export class CodexProvider {
|
|
|
152
152
|
}
|
|
153
153
|
async discoverModel({ signal } = {}) {
|
|
154
154
|
if (!this.model) { const models = await this.listModels({ signal }); this.model = models[0]?.id; }
|
|
155
|
-
if (!this.model) throw fail(`
|
|
155
|
+
if (!this.model) throw fail(`The Codex catalog did not advertise models for compatibility ${CODEX_CATALOG_VERSION}. Check access and adapter version; an empty list does not prove your plan has no models.`, 'MODEL_DISCOVERY_UNAVAILABLE');
|
|
156
156
|
return this.model;
|
|
157
157
|
}
|
|
158
158
|
async discoverModelCapabilities({ signal, force = false } = {}) {
|
|
@@ -177,24 +177,24 @@ export class CodexProvider {
|
|
|
177
177
|
getCapabilities() { return this.modelCapabilities?.model === this.model ? { ...this.modelCapabilities.capabilities, vision: this.modelCapabilities.capabilities.vision === true } : { text: true, tools: true, ...this.capabilitiesConfigured, vision: this.vision === 'on', video: false, web_search: false }; }
|
|
178
178
|
analyzeMedia(request) { return analyzeImages(this, request); }
|
|
179
179
|
async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, purpose = 'worker', maxTokens = this.maxTokens }) {
|
|
180
|
-
if (this.capabilitiesConfigured.tools === false && tools.length) throw fail('
|
|
180
|
+
if (this.capabilitiesConfigured.tools === false && tools.length) throw fail('The Codex profile does not enable tools; the request was not sent.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
181
181
|
await this.discoverModel({ signal });
|
|
182
182
|
const accountingRequest={system,messages,tools,maxTokens},identity={...accountingIdentity(this),projection_epoch:contextIdentity?.projection_epoch ?? null};
|
|
183
183
|
const schemaFrame=compileToolSchemas(tools,{provider:this.protocol,capabilities:{...(this.getCapabilities?.() || {}),...(this.toolSchemaCapabilities?{tool_schema:this.toolSchemaCapabilities}:{})},catalogue:schemaCatalog}); tools=schemaFrame.tools;
|
|
184
|
-
if (messages.some(message => Array.isArray(message.content) && message.content.some(block => block.type === 'image')) && !(await this.getVisionCapability({ signal })).supported) throw fail('
|
|
184
|
+
if (messages.some(message => Array.isArray(message.content) && message.content.some(block => block.type === 'image')) && !(await this.getVisionCapability({ signal })).supported) throw fail('The Codex catalog does not advertise vision for this model.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
|
|
185
185
|
let reasoning;
|
|
186
186
|
if (this.effort !== 'auto') {
|
|
187
187
|
const state = await this.discoverModelCapabilities({ signal });
|
|
188
|
-
if (!state.effort.levels.includes(this.effort)) throw fail(`
|
|
188
|
+
if (!state.effort.levels.includes(this.effort)) throw fail(`The model does not advertise effort ${this.effort}.`, 'MODEL_EFFORT_UNAVAILABLE');
|
|
189
189
|
reasoning = { effort: this.effort === 'off' ? 'none' : this.effort };
|
|
190
190
|
}
|
|
191
191
|
// Follow the upstream Codex request shape, which omits max_output_tokens.
|
|
192
192
|
// maxTokens remains a host accounting estimate, not a wire-enforced cap.
|
|
193
193
|
const body = JSON.stringify({ model: this.model, instructions: system || 'You are Comal, a coding assistant.', input: toCodexInput(messages), stream: true, store: false,
|
|
194
194
|
include: ['reasoning.encrypted_content'], ...(reasoning ? { reasoning } : {}),
|
|
195
|
-
...(tools.length ? { tools: tools.map(tool => ({ type: 'function', name: tool.name, description: tool.description, parameters: tool.input_schema })), tool_choice: 'auto', parallel_tool_calls: true } : {}) });
|
|
195
|
+
...(tools.length ? { tools: tools.map(tool => ({ type: 'function', name: tool.name, description: tool.description, strict: false, parameters: tool.input_schema })), tool_choice: 'auto', parallel_tool_calls: true } : {}) });
|
|
196
196
|
const account=result=>{result.usage_accounting=bindUsageAccounting(result.usage_accounting,accountingRequest,identity,body); result.schema_diagnostics=schemaFrame.diagnostics;result.schema_manifest=schemaFrame.manifest;return result;};
|
|
197
|
-
if (Buffer.byteLength(body) > 8 * 1024 * 1024) throw fail('
|
|
197
|
+
if (Buffer.byteLength(body) > 8 * 1024 * 1024) throw fail('The Codex context exceeds 8 MiB.');
|
|
198
198
|
const deadline = modelDeadline({ signal, timeoutMs: this.timeoutMs, idleTimeoutMs: this.idleTimeoutMs, purpose });
|
|
199
199
|
try {
|
|
200
200
|
const response = await this._fetch('responses', { method: 'POST', body, signal: deadline.signal });
|
|
@@ -212,7 +212,7 @@ export class CodexProvider {
|
|
|
212
212
|
onProgress({ text_chars: textChars, reasoning_chars: reasoningChars, tool_argument_chars: argumentChars, elapsed_ms: deadline.elapsedMs(), purpose });
|
|
213
213
|
}
|
|
214
214
|
deadline.signal.throwIfAborted();
|
|
215
|
-
if (!result) throw fail('
|
|
215
|
+
if (!result) throw fail('Codex streaming was interrupted; no tools were executed.', 'CODEX_RESPONSE_INCOMPLETE');
|
|
216
216
|
return account(result);
|
|
217
217
|
} catch (error) { if(error.usage_accounting)error.usage_accounting=bindUsageAccounting(error.usage_accounting,accountingRequest,identity,body);throw deadline.error(error); } finally { deadline.close(); }
|
|
218
218
|
}
|
package/src/compaction.mjs
CHANGED
|
@@ -155,7 +155,7 @@ export class WorkContextProjector {
|
|
|
155
155
|
const ceiling=Math.floor(hardBudget*.65),details={estimated:true,overhead_tokens:overhead,overhead_ceiling:ceiling,context_window:this.contextWindow,input_budget:hardBudget,output_reserve:outputTokens,margin:initial.margin,tools_count:tools.length,instructions_utf8_bytes:Buffer.byteLength(system || ''),tool_schema_utf8_bytes:Buffer.byteLength(JSON.stringify(tools)),generation_dispatched:false};
|
|
156
156
|
throw Object.assign(new Error(`Las instrucciones y esquemas de herramientas ocupan demasiado contexto: ~${overhead} tokens, techo ${ceiling} para ventana ${this.contextWindow} (${tools.length} herramientas; salida ${outputTokens}, margen ${initial.margin}). No se llamó al modelo. Reduce el catálogo/servidores MCP o las instrucciones; aumenta --context-window sólo si tu servidor y modelo lo permiten.`),{code:'CONTEXT_OVERHEAD',details});
|
|
157
157
|
}
|
|
158
|
-
const current = history.findLast(message => message.role === 'user' && typeof message.content === 'string'), protectedMessages = current ? [current] : [];
|
|
158
|
+
const current = history.findLast(message => message.role === 'user' && (typeof message.content === 'string' || Array.isArray(message.content) && message.content.some(block => block.type === 'text'))), protectedMessages = current ? [withoutImages(current)] : [];
|
|
159
159
|
const units = messageUnits(history);
|
|
160
160
|
const latest = units.slice(-8).findLast(unit => unit.messages[0].content?.some?.(block => block.type === 'tool_use')
|
|
161
161
|
&& !unit.messages[0].content.filter(block => block.type === 'tool_use').every(block => ['WorkBoard','WorkSnapshot','AuditStatus','HouseSnapshot','OperationCatalog','McpStatus','HouseContext','PluginStatus'].includes(block.name)));
|
package/src/goals.mjs
CHANGED
|
@@ -144,9 +144,9 @@ export class GoalRunner {
|
|
|
144
144
|
goal = await this.manager.read();
|
|
145
145
|
const delegatedDenied = recent.some(block => {
|
|
146
146
|
if (block.type !== 'tool_result') return false;
|
|
147
|
-
try { return JSON.parse(block.content).agents?.some(child => child.evidence?.some(item => item.is_error && /Permiso denegado/.test(item.output))) || false; } catch { return false; }
|
|
147
|
+
try { return JSON.parse(block.content).agents?.some(child => child.evidence?.some(item => item.is_error && /Permission denied|Permiso denegado/.test(item.output))) || false; } catch { return false; }
|
|
148
148
|
});
|
|
149
|
-
if (delegatedDenied || recent.some(b => b.type === 'tool_result' && b.is_error && /Permiso denegado/.test(b.content))) {
|
|
149
|
+
if (delegatedDenied || recent.some(b => b.type === 'tool_result' && b.is_error && /Permission denied|Permiso denegado/.test(b.content))) {
|
|
150
150
|
goal = await this.manager.write({ ...goal, status: 'paused', lastReason: 'Se requiere autorización para una herramienta; revisa los permisos y usa /goal resume.' }); break;
|
|
151
151
|
}
|
|
152
152
|
const verdict = await this.evaluator.evaluate({ condition: goal.condition, messages: session.historyMessages.slice(goal.messageBaseline || 0), signal, receipts: this.getReceipts ? await this.getReceipts(session, goal) : undefined });
|