@milpa/comal 0.1.0 → 0.1.1

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 (185) hide show
  1. package/README.md +30 -28
  2. package/backend/contracts/remote-commands-v1.json +125 -0
  3. package/bin/comal.mjs +16 -5
  4. package/docs/investigation.md +2 -2
  5. package/governance/chrome-task-v2.json +67 -0
  6. package/node_modules/@jitl/quickjs-ffi-types/LICENSE +21 -0
  7. package/node_modules/@jitl/quickjs-ffi-types/README.md +5 -0
  8. package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.mts +549 -0
  9. package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.ts +549 -0
  10. package/node_modules/@jitl/quickjs-ffi-types/dist/index.js +2 -0
  11. package/node_modules/@jitl/quickjs-ffi-types/dist/index.js.map +1 -0
  12. package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs +2 -0
  13. package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs.map +1 -0
  14. package/node_modules/@jitl/quickjs-ffi-types/package.json +36 -0
  15. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/LICENSE +47 -0
  16. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/README.md +93 -0
  17. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.d.ts +15 -0
  18. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.mjs +2 -0
  19. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cjs +2 -0
  20. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.cjs +2 -0
  21. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
  22. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.d.ts +15 -0
  23. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.mjs +2 -0
  24. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm +0 -0
  25. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm.map +1 -0
  26. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.mts +94 -0
  27. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.ts +94 -0
  28. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js +2 -0
  29. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js.map +1 -0
  30. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs +2 -0
  31. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs.map +1 -0
  32. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.mts +20 -0
  33. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.ts +20 -0
  34. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js +2 -0
  35. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js.map +1 -0
  36. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs +2 -0
  37. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs.map +1 -0
  38. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/package.json +61 -0
  39. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/LICENSE +47 -0
  40. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/README.md +88 -0
  41. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.d.ts +11 -0
  42. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.mjs +5430 -0
  43. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cjs +5485 -0
  44. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.cjs +5424 -0
  45. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  46. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.d.ts +11 -0
  47. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs +5541 -0
  48. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm +0 -0
  49. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm.map +1 -0
  50. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.mts +85 -0
  51. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.ts +85 -0
  52. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js +2 -0
  53. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js.map +1 -0
  54. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs +2 -0
  55. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs.map +1 -0
  56. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.mts +20 -0
  57. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.ts +20 -0
  58. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js +2 -0
  59. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js.map +1 -0
  60. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs +2 -0
  61. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs.map +1 -0
  62. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/package.json +61 -0
  63. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/LICENSE +47 -0
  64. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/README.md +89 -0
  65. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.d.ts +15 -0
  66. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.mjs +26 -0
  67. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cjs +25 -0
  68. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.cjs +25 -0
  69. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
  70. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.d.ts +15 -0
  71. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.mjs +30 -0
  72. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.wasm +0 -0
  73. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.mts +94 -0
  74. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.ts +94 -0
  75. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js +2 -0
  76. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js.map +1 -0
  77. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs +2 -0
  78. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs.map +1 -0
  79. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.mts +20 -0
  80. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.ts +20 -0
  81. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js +2 -0
  82. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js.map +1 -0
  83. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs +2 -0
  84. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs.map +1 -0
  85. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/package.json +61 -0
  86. package/node_modules/@jitl/quickjs-wasmfile-release-sync/LICENSE +47 -0
  87. package/node_modules/@jitl/quickjs-wasmfile-release-sync/README.md +82 -0
  88. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.d.ts +11 -0
  89. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.mjs +22 -0
  90. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cjs +21 -0
  91. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  92. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  93. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.d.ts +11 -0
  94. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.mjs +25 -0
  95. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm +0 -0
  96. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.mts +85 -0
  97. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.ts +85 -0
  98. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js +2 -0
  99. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js.map +1 -0
  100. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs +2 -0
  101. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs.map +1 -0
  102. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.mts +20 -0
  103. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.ts +20 -0
  104. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js +2 -0
  105. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js.map +1 -0
  106. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs +2 -0
  107. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs.map +1 -0
  108. package/node_modules/@jitl/quickjs-wasmfile-release-sync/package.json +61 -0
  109. package/node_modules/quickjs-emscripten/LICENSE +21 -0
  110. package/node_modules/quickjs-emscripten/README.md +928 -0
  111. package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs +2 -0
  112. package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs.map +1 -0
  113. package/node_modules/quickjs-emscripten/dist/index.d.mts +57 -0
  114. package/node_modules/quickjs-emscripten/dist/index.d.ts +57 -0
  115. package/node_modules/quickjs-emscripten/dist/index.global.js +7309 -0
  116. package/node_modules/quickjs-emscripten/dist/index.js +2 -0
  117. package/node_modules/quickjs-emscripten/dist/index.js.map +1 -0
  118. package/node_modules/quickjs-emscripten/dist/index.mjs +2 -0
  119. package/node_modules/quickjs-emscripten/dist/index.mjs.map +1 -0
  120. package/node_modules/quickjs-emscripten/dist/variants.d.mts +39 -0
  121. package/node_modules/quickjs-emscripten/dist/variants.d.ts +39 -0
  122. package/node_modules/quickjs-emscripten/dist/variants.js +2 -0
  123. package/node_modules/quickjs-emscripten/dist/variants.js.map +1 -0
  124. package/node_modules/quickjs-emscripten/dist/variants.mjs +2 -0
  125. package/node_modules/quickjs-emscripten/dist/variants.mjs.map +1 -0
  126. package/node_modules/quickjs-emscripten/package.json +69 -0
  127. package/node_modules/quickjs-emscripten-core/LICENSE +21 -0
  128. package/node_modules/quickjs-emscripten-core/README.md +341 -0
  129. package/node_modules/quickjs-emscripten-core/README.template.md +70 -0
  130. package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs +2 -0
  131. package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  132. package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs +7 -0
  133. package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  134. package/node_modules/quickjs-emscripten-core/dist/index.d.mts +2033 -0
  135. package/node_modules/quickjs-emscripten-core/dist/index.d.ts +2033 -0
  136. package/node_modules/quickjs-emscripten-core/dist/index.js +7 -0
  137. package/node_modules/quickjs-emscripten-core/dist/index.js.map +1 -0
  138. package/node_modules/quickjs-emscripten-core/dist/index.mjs +2 -0
  139. package/node_modules/quickjs-emscripten-core/dist/index.mjs.map +1 -0
  140. package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs +2 -0
  141. package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs.map +1 -0
  142. package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  143. package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  144. package/node_modules/quickjs-emscripten-core/package.json +49 -0
  145. package/package.json +6 -2
  146. package/src/agent.mjs +17 -6
  147. package/src/artifact-tools.mjs +3 -2
  148. package/src/cli.mjs +261 -199
  149. package/src/cloud-client.mjs +50 -8
  150. package/src/code-run-worker.mjs +70 -0
  151. package/src/code-run.mjs +97 -0
  152. package/src/codex-provider.mjs +27 -27
  153. package/src/compaction.mjs +1 -1
  154. package/src/goals.mjs +2 -2
  155. package/src/house.mjs +24 -24
  156. package/src/i18n/cli-es.mjs +97 -0
  157. package/src/i18n/es.mjs +227 -0
  158. package/src/i18n/index.mjs +48 -0
  159. package/src/image-attachments.mjs +263 -0
  160. package/src/model-controls.mjs +4 -4
  161. package/src/onboarding.mjs +91 -0
  162. package/src/openai-provider.mjs +24 -24
  163. package/src/permissions.mjs +52 -16
  164. package/src/provider-auth.mjs +30 -30
  165. package/src/provider-profiles.mjs +27 -27
  166. package/src/provider.mjs +30 -30
  167. package/src/remote-commands.mjs +11 -0
  168. package/src/remote-control.mjs +172 -0
  169. package/src/sessions.mjs +3 -3
  170. package/src/subagents.mjs +7 -3
  171. package/src/tool-packs.mjs +5 -2
  172. package/src/tools.mjs +81 -31
  173. package/src/tui/brand.mjs +2 -1
  174. package/src/tui/clipboard.mjs +5 -4
  175. package/src/tui/composer.mjs +77 -11
  176. package/src/tui/controller.mjs +110 -29
  177. package/src/tui/discovery.mjs +105 -100
  178. package/src/tui/markdown.mjs +10 -9
  179. package/src/tui/message-queue.mjs +27 -9
  180. package/src/tui/screen.mjs +209 -118
  181. package/src/vision.mjs +31 -3
  182. package/src/work-assertions.mjs +7 -7
  183. package/src/work-plan.mjs +104 -14
  184. package/src/work-recovery-proof.mjs +13 -1
  185. package/src/workflows/chrome-task.mjs +24 -7
@@ -8,17 +8,59 @@ 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 requiere un origen sin ruta ni credenciales.');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 requiere HTTPS.');return url.origin;}
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('Las credenciales de Comal deben ser un archivo privado (0600) de tu usuario.');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('El archivo de credenciales cambió o es demasiado grande.');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('El origen solicitado no coincide con la sesión guardada de Comal. Inicia sesión en ese origen.');if(typeof value.token!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(value.token))throw Error('Credencial de Comal inválida. Vuelve a iniciar sesión.');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('El directorio de credenciales de Comal debe ser privado (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('El transporte de laboratorio de Comal está desactivado en producción.');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(30000)]):AbortSignal.timeout(30000),maxBytes:2097152});if(response.headers.get('content-length')>2097152)throw Error('Respuesta de Comal demasiado grande.');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('Respuesta de Comal demasiado grande.');}parts.push(Buffer.from(value));}}finally{reader.releaseLock();}}let body;try{body=JSON.parse(Buffer.concat(parts).toString('utf8'));}catch{throw Error('Comal devolvió una respuesta inválida (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('Conecta tu cuenta con comal cloud login.');if(options.expectedAccount&&(credential.origin!==options.expectedAccount.origin||credential.user_id!==options.expectedAccount.user_id))throw Error('La cuenta de Comal cambió durante la publicación. Revisa el destino y vuelve a publicar.');if(credential.expires_at<=Date.now()/1000)throw Error('Tu sesión de Comal expiró. Usa comal cloud login.');return this.request(credential.origin,operation,input,credential.token,options.signal);}
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('Contrato de conexión CLI inválido.');const uri=new URL(flow.verification_uri_complete);if(uri.origin!==origin||uri.pathname!=='/connect')throw Error('Comal devolvió un destino de autenticación diferente.');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('La conexión fue denegada en Comal.');if(result.status!=='authenticated'||!/^[A-Za-z0-9_-]{43}$/.test(result.access_token)||!Number.isSafeInteger(result.expires_at))throw Error('Comal devolvió un intercambio inválido.');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('El código de Comal expiró. Inicia la conexión de nuevo.');}
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('La publicación debe estar dentro del workspace de esta sesión.');}const files=[];let bytes=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(entry.isSymbolicLink())throw Error('No se publican enlaces simbólicos: '+name);if(entry.isDirectory()){await visit(name);continue;}if(!entry.isFile()||!staticTypes.has(path.extname(entry.name).slice(1).toLowerCase()))throw Error('El directorio debe contener sólo archivos web estáticos: '+name);if(files.length>=256)throw Error('El artifact excede 256 archivos.');const current=await realpath(full);if(!current.startsWith(base+path.sep))throw Error('Un archivo sale del directorio de publicación.');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('El artifact excede 8 MiB.');const content=await handle.readFile();bytes+=content.length;if(bytes>8388608)throw Error('El artifact excede 8 MiB.');files.push({path:name,content_base64:content.toString('base64'),sha256:hash(content)});}finally{await handle.close();}}}await visit();if(!files.length)throw Error('El directorio de publicación está vacío.');return files;}
24
- export async function publishArtifact(client,{directory,workspaceRoot,...metadata},options={}){const files=await bundleArtifact(directory,{workspaceRoot});const account=await client.credentials();if(!account)throw Error('Conecta tu cuenta con comal cloud login.');const input={...metadata,files};input.request_id='comal_'+hash(JSON.stringify({origin:account.origin,user:account.user_id,input}));return client.invoke('artifacts.publish',input,{...options,expectedAccount:{origin:account.origin,user_id:account.user_id}});}
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()+90000;
45
+ while(result.status!=='ready'&&Date.now()<deadline){if(result.status==='failed')throw Error('Gumlet could not process this video. Inspect media '+result.id+'.');await delay(2000,null,{signal:options.signal});result=await client.invoke('media.inspect',{media_id:result.id},authority);}
46
+ if(result.status!=='ready')throw Error('Video '+result.id+' is still processing. Retry the same source to reconcile it without another upload.');
47
+ return result;
48
+ }
49
+ export async function uploadArtifactVideo(client,{file,workspaceRoot,...metadata},options={}){return sendArtifactVideo(client,await captureArtifactVideo(file,workspaceRoot),metadata,options);}
50
+ export async function publishArtifact(client,{directory,workspaceRoot=directory,...metadata},options={}){
51
+ const account=await client.credentials();if(!account)throw Error('Connect your account with comal cloud login.');
52
+ const authority={...options,expectedAccount:{origin:account.origin,user_id:account.user_id}},media=[],videos=[];
53
+ const files=await bundleArtifact(directory,{workspaceRoot,onVideo:async(file,name)=>{
54
+ videos.push({content:await captureArtifactVideo(file,workspaceRoot),path:name.slice(0,-4)+'.player.html'});
55
+ }});
56
+ if(metadata.entrypoint?.toLowerCase().endsWith('.mp4'))metadata.entrypoint=metadata.entrypoint.slice(0,-4)+'.player.html';
57
+ if(videos.length){
58
+ 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);}
59
+ 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.');
60
+ 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.');
61
+ }
62
+ for(const capture of videos){const video=await sendArtifactVideo(client,capture.content,metadata,authority);media.push({path:capture.path,media_id:video.id});}
63
+ const input={...metadata,files,...(media.length?{media}:{})};
64
+ input.request_id='comal_'+hash(JSON.stringify({origin:account.origin,user:account.user_id,input}));
65
+ return client.invoke('artifacts.publish',input,authority);
66
+ }
@@ -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
+ });
@@ -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
+ }
@@ -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 = 'Respuesta Codex incompleta';
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 rechazó el esquema de herramientas'; }
26
- else if (metadata.type === 'invalid_request_error' || metadata.code === 'invalid_request_error') { code = 'CODEX_REQUEST_REJECTED'; message = 'Codex rechazó la petición'; }
27
- else if (upstream) { code = 'CODEX_RESPONSE_FAILED'; message = 'Codex devolvió un error'; }
28
- if (httpStatus === 401) { code = 'PROVIDER_AUTH_REQUIRED'; message = 'Codex requiere login'; }
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 se ejecutaron herramientas de esta respuesta.`, code), {
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 devolvió un cuerpo vacío.', 'CODEX_RESPONSE_INCOMPLETE');
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 devolvió un cuerpo vacío o incompleto.', 'CODEX_RESPONSE_INCOMPLETE');
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('Formato de respuesta Codex desconocido; no se ejecutaron herramientas.');
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('La respuesta Codex excedió el límite de bytes.');text+=decoder.decode(chunk,{stream:true});}
63
- try{return JSON.parse(text+decoder.decode());}catch{throw fail('Codex devolvió JSON inválido; no se ejecutaron herramientas.');}
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('Multimedia no admitida o referencia sin resolver.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
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('Llamada Codex inválida.');
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 devolvió una respuesta vacía.');
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 requiere AuthManager y authProvider codex.');
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('Configuración de capacidades Codex inválida.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
121
- if (this.capabilitiesConfigured.text === false || this.capabilitiesConfigured.video === true || this.capabilitiesConfigured.web_search === true) throw fail('El adaptador Codex requiere texto y no implementa video/web_search nativos.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
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('Visión Codex inválida.');
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('Catálogo Codex demasiado grande.');
143
- let data; try { data = JSON.parse(text); } catch { throw fail('Catálogo Codex inválido.'); }
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 no anunció modelos.', 'MODEL_DISCOVERY_UNAVAILABLE');
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(`El catálogo Codex no anunció modelos para compatibilidad ${CODEX_CATALOG_VERSION}. Revisa acceso y versión del adaptador; una lista vacía no prueba que tu plan carezca de modelos.`, 'MODEL_DISCOVERY_UNAVAILABLE');
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,15 +177,15 @@ 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('El perfil Codex no habilita tools; no se envió la petición.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
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('El catálogo Codex no anuncia visión para este modelo.', 'PROVIDER_CAPABILITY_UNAVAILABLE');
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(`El modelo no anuncia effort ${this.effort}.`, 'MODEL_EFFORT_UNAVAILABLE');
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.
@@ -194,7 +194,7 @@ export class CodexProvider {
194
194
  include: ['reasoning.encrypted_content'], ...(reasoning ? { reasoning } : {}),
195
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 } : {}) });
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('El contexto Codex excede 8 MiB.');
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('Streaming Codex interrumpido; no se ejecutaron herramientas.', 'CODEX_RESPONSE_INCOMPLETE');
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
  }
@@ -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 });