beeos-claw 0.6.69 → 0.6.70
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 +13 -0
- package/dist/a2a/tools.js +1 -1
- package/dist/acp-gateway/beeos-file-resolver.js +1 -1
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/local-session-history.js +1 -1
- package/dist/acp-gateway/mcp-desired-state-controller.js +1 -1
- package/dist/acp-gateway/mcp-desired-state-store.js +1 -1
- package/dist/acp-gateway/mcp-service.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/agent-auth.js +1 -0
- package/dist/agent-sync.js +1 -1
- package/dist/auto-reply-tokens.js +1 -1
- package/dist/automation/tools.js +1 -1
- package/dist/bridge-discovery.js +1 -0
- package/dist/canvas/relay-client.js +1 -1
- package/dist/canvas/static-html.js +1 -1
- package/dist/canvas/tools.js +1 -1
- package/dist/canvas/ws-provider.js +1 -1
- package/dist/canvas-orchestrator.js +1 -1
- package/dist/files/fetch-tool.js +1 -1
- package/dist/files/list-tool.js +1 -1
- package/dist/gateway-client.js +1 -1
- package/dist/im-inbound-handler.js +1 -1
- package/dist/im-runtime-dispatch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-connector-proxy.js +1 -0
- package/dist/media/arouter-config.js +1 -1
- package/dist/media/image-provider.js +1 -1
- package/dist/media/video-provider.js +1 -1
- package/dist/message-client-composition.js +1 -1
- package/dist/message-projection-inbound-consumer.js +1 -1
- package/dist/plugin-registration-plan.js +1 -1
- package/dist/runtime/agent-gateway-runtime-attempt-authorization.js +1 -0
- package/dist/runtime/agent-projection-production.js +1 -1
- package/dist/runtime/assistant-message-commit-router.js +1 -1
- package/dist/runtime/callback-queue.js +1 -1
- package/dist/runtime/conversation-title.js +1 -1
- package/dist/runtime/core-execution-attempt-context.js +1 -1
- package/dist/runtime/deepseek-reasoning-stream.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/dist/runtime/message-service-conversation-clear-http-authority.js +1 -1
- package/dist/runtime/message-service-conversation-run-boundary.js +1 -1
- package/dist/runtime/message-service-cron-delivery-http-authority.js +1 -1
- package/dist/runtime/message-service-runtime-authenticated-fetch.js +1 -1
- package/dist/runtime/message-service-runtime-claim-authority.js +1 -1
- package/dist/runtime/message-service-runtime-invocation-authority.js +1 -0
- package/dist/runtime/message-service-runtime-message-channel.js +1 -1
- package/dist/runtime/message-service-session-model-http-authority.js +1 -1
- package/dist/runtime/message-service-session-reasoning-http-authority.js +1 -0
- package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
- package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
- package/dist/runtime/ms-runtime-rpc-adapter.js +1 -1
- package/dist/runtime/openclaw-native-sessions.js +1 -0
- package/dist/runtime/openclaw-question-adapter.js +1 -0
- package/dist/runtime/openclaw-runtime-capabilities.js +1 -1
- package/dist/runtime/openclaw-session-write-quiescence.js +1 -1
- package/dist/runtime/question-wire.js +1 -0
- package/dist/runtime/reply-router.js +1 -1
- package/dist/runtime/runtime-command-dispatcher.js +1 -1
- package/dist/runtime/runtime-command-journal.js +1 -1
- package/dist/runtime/runtime-jwks-verifier.js +1 -0
- package/dist/runtime/runtime-local-parity-matrix.js +1 -1
- package/dist/runtime/runtime-message-channel-contract.js +1 -1
- package/dist/runtime/runtime-operation-resource-resolver.js +1 -1
- package/dist/runtime/runtime-parity-manifest-digest.js +1 -1
- package/dist/runtime/runtime-parity-manifest.js +1 -1
- package/dist/runtime/runtime-production-authority.js +1 -1
- package/dist/runtime/runtime-reasoning.js +1 -0
- package/dist/runtime/runtime-registration.js +1 -0
- package/dist/runtime/runtime-service-contract-compatibility.js +1 -1
- package/dist/runtime/runtime-service-production-candidate.js +1 -1
- package/dist/runtime/runtime-session-control-service.js +1 -1
- package/dist/runtime/runtime-skills-service.js +1 -1
- package/dist/runtime/runtime-transport-failure-policy.js +1 -0
- package/dist/runtime/session-mutation-coordinator.js +1 -1
- package/dist/runtime/session-new-runtime-delivery-bridge.js +1 -1
- package/dist/runtime/session-set-model-mapping.js +1 -1
- package/dist/runtime/split-runtime-authorization-verifier.js +1 -0
- package/dist/runtime/write-runtime-failure.js +1 -1
- package/dist/terminal/runtime-terminal-outbound-registration.js +1 -1
- package/dist/terminal/runtime-terminal-service.js +1 -1
- package/dist/terminal/terminal-authority-admission.js +1 -1
- package/dist/terminal/terminal-authority-contract.js +1 -1
- package/dist/terminal/terminal-lease-bound-bridge-client.js +1 -1
- package/dist/upload/desktop-screenshot-tool.js +1 -1
- package/dist/upload/finalize-hook.js +1 -1
- package/dist/upload/tool-artifact-enrichment.js +1 -1
- package/dist/upload/tool.js +1 -1
- package/dist/upload/upload-file.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -12
- package/runtime-service-wire-identity.v1.json +12 -12
- package/scripts/runtime-blackbox-worker-main.mjs +2 -5
- package/dist/approval/bridge.js +0 -1
- package/dist/approval/presentation.js +0 -1
- package/dist/approval/protocol.generated.js +0 -1
- package/dist/chat-envelope.js +0 -1
- package/dist/connectors/harness-connector-mcp.js +0 -1
- package/dist/question/bridge.js +0 -1
- package/dist/runtime/chat-handler-port.js +0 -1
- package/dist/runtime/native-cloud-chat-composition.js +0 -1
- package/dist/runtime/native-cloud-prompt-bridge.js +0 -1
- package/dist/runtime/native-input-files-http.js +0 -1
- package/dist/runtime/native-input-files.js +0 -1
- package/dist/runtime/native-output-file-attempt.js +0 -1
- package/dist/runtime/native-prompt-authority.js +0 -1
- package/dist/runtime/native-prompt-preflight.js +0 -1
- package/dist/runtime/native-session-new-bridge.js +0 -1
- package/dist/runtime/openclaw-session-model.js +0 -1
- package/dist/runtime/runtime-error-part.js +0 -1
- package/dist/upload/native-output-tool-hook.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{createSessionModelRefs}from"./openclaw-session-model.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;modelMutations;constructor(e,t,i,s){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i,this.modelMutations=s}modelRefs(){return createSessionModelRefs()}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{sessionKey:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s=openClawSessionModePatch(t),n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const r=this.modelRefs().toPlatform(o),a=r.indexOf("/");return{modelId:r,...a>0?{modelProvider:r.slice(0,a)}:{}}}async setModel(e,t,i,s){const n=null===t?null:this.modelRefs().toOpenClaw(t),o=s?.attempt.operationId;let r=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s));const a=await(this.modelMutations?.acquireInternal({sessionKey:r.key,label:"session model mutation"}));try{if(a){const t=await this.resolveSetModelKey(e,s);if(t.key!==a.sessionKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model target changed while waiting for its physical session gate");r=t}const l=async()=>await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:r.key,model:n,mustExist:!r.materialized}));let d;try{d=await l()}catch(t){if(r.materialized||!isMissingOpenClawStoreSession(t,r.key))throw t;r=await modelSwitchTimingSpan(o,"materialize_set_model_key",async()=>await this.materializeSetModelKey(e,r.key,s)),d=await l()}const{key:c,materialized:m}=r,u=resolveModelPatch(d,c,t,this.modelRefs()),h=u.effectiveModelId;if(null===t&&(!i||h!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(m){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(c))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(c)}return u}finally{this.modelMutations?.releaseBeforeRuntime(a)}}async prepareNativeModel(e,t,i,s,n){await i();const o=void 0===t?null:this.modelRefs().toOpenClaw(t),r=void 0===s?void 0:openClawSessionModePatch(s),a=openClawPermissionModePatch(n),l=await this.gateway().request("sessions.patch",{key:e,model:o,...r,...a});await i();const d=l;if(!0!==d?.ok||d.key!==e)throw new RuntimeCommandError("OUTCOME_UNKNOWN","native model patch did not acknowledge the exact qualified session");if(r&&!isAuthoritativeSetModeAck(l,e,r))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");if(!isAuthoritativePermissionModeAck(l,a))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the effective permission mode");return resolveModelPatch(l,e,t??null,this.modelRefs())}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}return i?{key:i,materialized:!1}:await this.materializeSetModelKey(e,void 0,t)}async materializeSetModelKey(e,t,i){if(!("service"===i?.attempt.origin&&void 0!==i.attempt.verifiedMessageGrant&&i&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${t??e}`)}const s=await this.liveSessionKeys.materializeMissingModelKey(e,i);if(t&&s!==t)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:s,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function isMissingOpenClawStoreSession(e,t){return e instanceof Error&&e.message===`No session found: ${t}`}function resolveModelPatch(e,t,i,s){const n=null===i?null:s.toOpenClaw(i);if(!isAuthoritativeSetModelAck(e,t,n,i))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const o=e.resolved.model,r=s.toPlatform(o);if(null!==i&&r!==i&&o!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");return{...null===i?{}:{modelOverrideId:i},effectiveModelId:r,...e.resolved.modelProvider?{modelProvider:e.resolved.modelProvider}:{}}}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}export function openClawSessionModePatch(e){const t="verbose"===e||"detailed"===e?{verboseLevel:"full"}:"reasoning"===e||"thinking"===e?{reasoningLevel:"stream"}:"concise"===e||"default"===e?{verboseLevel:"default"}:null;if(!t)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${e}`);return t}export function openClawPermissionModePatch(e){return{permissionMode:"full"===e?"full":null}}function isAuthoritativePermissionModeAck(e,t){const i=e?.entry;return!(!i||"object"!=typeof i||Array.isArray(i))&&i.permissionMode===(t.permissionMode??void 0)}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o.model;return"string"==typeof r&&0!==r.length&&r.trim()===r&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}
|
|
1
|
+
import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{AROUTER_PROVIDER_ID}from"../media/arouter-config.js";import{getOpenClawNativeSessions}from"./openclaw-native-sessions.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setReasoning":{if(!this.metadata.setReasoning)throw new RuntimeCommandError("INVALID_PARAMS","reasoning is not supported");const e=s.params;return await this.metadata.setReasoning(n,e.modelId,e.reasoningOverrideId,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setReasoning"===e.name){if("service"===i?.origin){if(!i.sessionReasoningProjection)throw new Error("Message Service reasoning projection unavailable");return Object.freeze({state:"pending",revision:"0"})}return}if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;constructor(e,t,i){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{key:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t,...await this.requireSessionIdentity(s)}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s="verbose"===t||"detailed"===t?{verboseLevel:"full"}:"reasoning"===t||"thinking"===t?{reasoningLevel:"stream"}:"concise"===t||"default"===t?{verboseLevel:"default"}:null;if(!s)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${t}`);const n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s,...await this.requireSessionIdentity(n)}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async setReasoning(e,t,i,s){let n=this.liveSessionKeys?await this.resolveSetModelKey(e,s):{key:await this.requireExistingOpenClawStoreKey(e,s),materialized:!1};openClawSessionKeysFromList(await this.gateway().request("sessions.list",{includeDerivedTitles:!0})).has(n.key)||(n=await this.materializeSetModelKey(e,n.key,s));const o=n.key,r=await this.requireSessionIdentity(o),a=await this.gateway().request("sessions.list",{includeDerivedTitles:!0}),l=a.sessions?.find(e=>(e.key??e.sessionKey)===o);if((l?.model?toPlatformModelId(l.model.includes("/")?l.model:`${l.modelProvider}/${l.model}`):null)!==t)throw new RuntimeCommandError("INVALID_PARAMS","model changed; refresh reasoning options");const d=await this.gateway().request("models.list",{agentId:runtimeAgentId(o)}),c=d.models?.find(e=>"string"==typeof e.id&&toPlatformModelId(e.id.includes("/")?e.id:`${e.provider}/${e.id}`)===t),m=c?runtimeReasoningOptions(c):void 0;if(!m||null!==i&&!m.levels.some(e=>e.id===i))throw new RuntimeCommandError("INVALID_PARAMS","unsupported reasoning level");const u=await this.gateway().request("sessions.patch",{key:o,thinkingLevel:i,...r}),y=u?.resolved?.model?toPlatformModelId(u.resolved.model.includes("/")?u.resolved.model:`${u.resolved.modelProvider}/${u.resolved.model}`):null;if(!u||!0!==u.ok||u.key!==o||y!==t||!u.entry||(null===i?null!=u.entry.thinkingLevel:u.entry.thinkingLevel!==i))throw new RuntimeCommandError("OUTCOME_UNKNOWN","thinking configuration was not acknowledged");return n.materialized&&this.liveSessionKeys?.registerMaterializedModelKey?.(o),{modelId:t,reasoningOverrideId:i,effectiveReasoningId:i??m.defaultLevel,defaultReasoningId:m.defaultLevel}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const r=toPlatformModelId(o),a=r.indexOf("/");return{modelId:r,...a>0?{modelProvider:r.slice(0,a)}:{}}}async setModel(e,t,i,s){const n=null===t?null:toOpenClawSessionModel(t),o=s?.attempt.operationId;let r=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s));const a=await this.readSessionIdentity(r.key);a||r.materialized||(r=await this.materializeSetModelKey(e,r.key,s));const l=await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:r.key,model:n,thinkingLevel:null,...a?{expectedSessionId:a.sessionId,...a.lifecycleRevision?{expectedLifecycleRevision:a.lifecycleRevision}:{}}:{}})),{key:d,materialized:c}=r;if(!isAuthoritativeSetModelAck(l,d,n,t))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const m=l.resolved.model,u=toPlatformModelId(m);if(null===t&&(!i||u!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(null!==t&&u!==t&&m!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");if(c){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(d))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(d)}return{...null===t?{}:{modelOverrideId:t},effectiveModelId:u,...l.resolved?.modelProvider?{modelProvider:l.resolved.modelProvider}:{}}}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async readSessionIdentity(e){const t=getOpenClawNativeSessions();if(t)return t.getEntry(runtimeAgentId(e),e);const i=await this.gateway().request("sessions.list",{includeDerivedTitles:!0}),s=i?.sessions?.filter(t=>(t.key??t.sessionKey)===e)??[];if(0===s.length)return;const n=s[0];if(1!==s.length||"string"!=typeof n.sessionId||!n.sessionId.trim())throw new RuntimeCommandError("INVALID_PARAMS","OpenClaw session identity is missing or ambiguous");return{sessionId:n.sessionId,..."string"==typeof n.lifecycleRevision&&n.lifecycleRevision?{lifecycleRevision:n.lifecycleRevision}:{}}}async requireSessionIdentity(e){const t=await this.readSessionIdentity(e);if(!t)throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${e}`);return{expectedSessionId:t.sessionId,...t.lifecycleRevision?{expectedLifecycleRevision:t.lifecycleRevision}:{}}}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}return i?{key:i,materialized:!1}:await this.materializeSetModelKey(e,void 0,t)}async materializeSetModelKey(e,t,i){if(!("service"===i?.attempt.origin&&void 0!==i.attempt.verifiedMessageGrant&&i&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${t??e}`)}const s=await this.liveSessionKeys.materializeMissingModelKey(e,i);if(t&&s!==t)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:s,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}function toOpenClawSessionModel(e){return e.startsWith(`${AROUTER_PROVIDER_ID}/`)?e:`${AROUTER_PROVIDER_ID}/${e}`}function toPlatformModelId(e){const t=`${AROUTER_PROVIDER_ID}/`;return e.startsWith(t)?e.slice(t.length):e}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o.model;return"string"==typeof r&&0!==r.length&&r.trim()===r&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}import{runtimeReasoningOptions}from"./runtime-reasoning.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{access,mkdir,rename,rm,writeFile}from"node:fs/promises";import{homedir}from"node:os";import{
|
|
1
|
+
import{access,mkdir,rename,rm,writeFile}from"node:fs/promises";import{homedir}from"node:os";import{dirname,join,resolve,sep}from"node:path";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";export class RuntimeSkillsService{deps;constructor(e){this.deps=e}async execute(e,t,i){const a=e,r=this.requireGateway(),s=a.context.agent.runtimeLocalKey;if("skills.list"===a.name){const e=await r.request("skills.status",{agentId:s});return{skills:normalizeSkills(e?.skills)}}if("skills.setEnabled"===a.name){const e=a;return await r.request("skills.update",{skillKey:e.params.skillKey,enabled:e.params.enabled,agentId:s}),{skillKey:e.params.skillKey,enabled:e.params.enabled}}const n=await this.skillsRoot(r,s);if("skills.uninstall"===a.name){const e=validateSkillKey(a.params.skillKey);return await rm(contained(n,e),{recursive:!0,force:!0}),{removed:!0}}const o=a,l=await i.resources.readVerifiedArtifact(o.params.artifact),c=validateSkillKey(l.manifest?.slug??""),m=contained(n,c),d=contained(n,`.${c}.${o.context.operationId}.tmp`),u=contained(n,`.${c}.${o.context.operationId}.backup`);!await exists(m)&&await exists(u)&&await rename(u,m),await rm(d,{recursive:!0,force:!0}),await mkdir(d,{recursive:!0});try{for(const[e,t]of l.workspaceFiles){const i=contained(d,normalizeArtifactPath(e));await mkdir(dirname(i),{recursive:!0}),await writeFile(i,t,{mode:384})}await rm(u,{recursive:!0,force:!0}),await exists(m)&&await rename(m,u),await rename(d,m),await rm(u,{recursive:!0,force:!0})}catch(e){throw await rm(d,{recursive:!0,force:!0}).catch(()=>{}),!await exists(m)&&await exists(u)&&await rename(u,m).catch(()=>{}),e}return{installed:!0}}requireGateway(){const e=this.deps.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}async skillsRoot(e,t){const i=this.deps.getOpenclawHome?.()??process.env.OPENCLAW_HOME??join(homedir(),".openclaw");if("main"===t)return join(i,"skills");const a=await e.request("agents.list",{}),r=a?.agents?.find(e=>e.id===t)?.workspace;return join(r||join(i,`workspace-${t}`),"skills")}}async function exists(e){try{return await access(e),!0}catch{return!1}}function validateSkillKey(e){if(!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(e))throw new RuntimeCommandError("INVALID_PARAMS","invalid skill key");return e}function normalizeArtifactPath(e){const t=e.replaceAll("\\","/").replace(/^\.\//,"");if(!t||t.startsWith("/")||t.split("/").includes(".."))throw new RuntimeCommandError("INVALID_PARAMS",`invalid skill artifact path: ${e}`);return t}function contained(e,t){const i=resolve(e),a=resolve(i,t);if(a!==i&&!a.startsWith(`${i}${sep}`))throw new RuntimeCommandError("INVALID_PARAMS","skill path escapes workspace");return a}function normalizeSkills(e){return(e??[]).flatMap(e=>{if(!e||"object"!=typeof e)return[];const t=e,i="string"==typeof t.skillKey?t.skillKey:"string"==typeof t.name?t.name:"";return i?[{skillKey:i,slug:"string"==typeof t.slug?t.slug:i,name:"string"==typeof t.name?t.name:i,description:"string"==typeof t.description?t.description:"",enabled:!0!==t.disabled,..."string"==typeof t.emoji?{emoji:t.emoji}:{},..."boolean"==typeof t.bundled?{bundled:t.bundled}:{}}]:[]})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class RuntimeTransportFailurePolicy{threshold;consecutiveFailures=0;constructor(e=3){if(this.threshold=e,!Number.isSafeInteger(e)||e<=0)throw new Error("transport failure threshold must be a positive integer")}recordSuccess(){this.consecutiveFailures=0}recordFailure(){return this.consecutiveFailures+=1,!(this.consecutiveFailures<this.threshold)&&(this.consecutiveFailures=0,!0)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_QUIESCENCE_TIMEOUT_MS=3e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationQuiescenceTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;onRecoveryRequired;constructor(e,r,o,t={}){this.gate=e,this.quiescence=r,this.logger=o,this.observationTimeoutMs=t.observationTimeoutMs??13e4,this.cancellationQuiescenceTimeoutMs=t.cancellationQuiescenceTimeoutMs??3e4,this.cancellationTerminalObservationTimeoutMs=t.cancellationTerminalObservationTimeoutMs??3e4,this.setTimer=t.setTimer??((e,r)=>{const o=setTimeout(e,r);return o.unref?.(),o}),this.clearTimer=t.clearTimer??(e=>clearTimeout(e)),this.onRecoveryRequired=t.onRecoveryRequired}tryStartInteractive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"attachable"),o}tryStartExclusive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"exclusive"),o}async acquireInternal(e){const r=await this.gate.acquireInternal(e);return this.register(r,"internal","attachable"),r}bindRun(e,r){if(!e||!r)return;const o=this.states.get(e.operationId);o&&"recovery_required"!==o.phase&&(o.runId=r)}attachOrStartExternal(e,r,o){const t=this.stateForSession(e.sessionKey);if(t)return t.recovery?{ok:!1,reason:"session_busy",recovery:t.recovery}:"exclusive"===t.attachmentPolicy?{ok:!1,reason:"session_busy"}:t.runId&&o&&t.runId!==o?{ok:!1,reason:"run_conflict",recovery:this.fence(t,"run_conflict")}:(!t.runId&&o&&(t.runId=o),{ok:!0,ownership:"observer",ownerOperationId:t.token.operationId,attached:!0});const n=this.tryStartInteractive(e,r);return n.ok?(o&&this.bindRun(n.token,o),{ok:!0,ownership:"owner",token:n.token,attached:!1}):{ok:!1,reason:n.reason}}requestCancel(e,r){const o=this.requireState(e);if(o.recovery)return Promise.resolve({ok:!1,recovery:o.recovery});if(o.cancelPromise)return o.cancelPromise;if(o.terminalKind||o.convergencePromise)return Promise.resolve({ok:!1,reason:"terminal_already_observed"});o.phase="cancelling",o.cancelObservationPromise=new Promise(e=>{o.resolveCancelObservation=e});const t=o.cancelObservationPromise;o.cancelObservationTimer=this.setTimer(()=>{o.terminalKind||o.recovery||this.fence(o,"terminal_not_observed")},this.cancellationTerminalObservationTimeoutMs);const n=Promise.resolve().then(()=>r.chatAbort(e.sessionKey,o.runId)).then(r=>(r.aborted?(o.cancelAcknowledged=!0,this.logger.debug?.(`[session-mutation] remote abort acknowledged sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`)):this.logger.debug?.(`[session-mutation] remote abort not acknowledged; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:r.aborted,terminalObservation:t})).catch(r=>(this.logger.error?.(`[session-mutation] remote abort transport failed; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} error=${r instanceof Error?r.message:String(r)}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:!1,terminalObservation:t}));return o.cancelPromise=n,n}requestCancelForSession(e,r){const o=this.stateForSession(e);return o?"exclusive"===o.attachmentPolicy?Promise.resolve({ok:!1,reason:"not_cancellable"}):this.requestCancel(o.token,r):Promise.resolve({ok:!1,reason:"no_active_mutation"})}converge(e,r){const o=this.requireState(e);return o.convergencePromise||(o.terminalKind=r,this.resolveTerminalObserved(o),o.convergencePromise=this.proveConvergence(o)),o.convergencePromise}release(e){const r=this.states.get(e.operationId);r&&r.token.sessionKey===e.sessionKey&&"ready_to_release"===r.phase&&(this.clearCancellationTimer(r),this.gate.release(r.token),this.states.delete(e.operationId))}releaseBeforeRuntime(e){if(!e)return;const r=this.states.get(e.operationId);r?.recovery||(r&&this.clearCancellationTimer(r),this.gate.release(e),this.states.delete(e.operationId))}requireRecovery(e,r,o){const t=this.requireState(e);return o&&(t.terminalKind=o),this.fence(t,r)}snapshot(e){for(const r of this.states.values())if(r.token.sessionKey===e)return{sessionKey:e,operationId:r.token.operationId,surface:r.surface,attachmentPolicy:r.attachmentPolicy,phase:r.phase,...r.runId?{runId:r.runId}:{},...r.terminalKind?{terminalKind:r.terminalKind}:{},cancelAcknowledged:r.cancelAcknowledged,...r.recovery?{recovery:r.recovery}:{}}}gateSnapshot(e){return this.gate.snapshot(e)}fenceActive(e){for(const r of this.states.values())r.recovery||this.fence(r,e)}register(e,r,o){this.states.set(e.operationId,{token:e,surface:r,attachmentPolicy:o,phase:"active",cancelAcknowledged:!1})}requireState(e){const r=this.states.get(e.operationId);if(!r||r.token.sessionKey!==e.sessionKey)throw new Error("session_mutation_not_registered");return r}stateForSession(e){for(const r of this.states.values())if(r.token.sessionKey===e)return r}async proveConvergence(e){if(e.recovery)return{ok:!1,recovery:e.recovery};if(!e.terminalKind)return{ok:!1,recovery:this.fence(e,"terminal_not_observed")};if(e.recovery)return{ok:!1,recovery:e.recovery};e.phase="awaiting_quiescence";const r="cancelled"===e.terminalKind?this.cancellationQuiescenceTimeoutMs:this.observationTimeoutMs;let o;try{o=await this.quiescence.waitForQuiescence(e.token.sessionKey,r)}catch{return e.recovery?{ok:!1,recovery:e.recovery}:{ok:!1,recovery:this.fence(e,"quiescence_failed")}}if(e.recovery)return{ok:!1,recovery:e.recovery};if(o.quiescent
|
|
1
|
+
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_QUIESCENCE_TIMEOUT_MS=3e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationQuiescenceTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;onRecoveryRequired;constructor(e,r,o,t={}){this.gate=e,this.quiescence=r,this.logger=o,this.observationTimeoutMs=t.observationTimeoutMs??13e4,this.cancellationQuiescenceTimeoutMs=t.cancellationQuiescenceTimeoutMs??3e4,this.cancellationTerminalObservationTimeoutMs=t.cancellationTerminalObservationTimeoutMs??3e4,this.setTimer=t.setTimer??((e,r)=>{const o=setTimeout(e,r);return o.unref?.(),o}),this.clearTimer=t.clearTimer??(e=>clearTimeout(e)),this.onRecoveryRequired=t.onRecoveryRequired}tryStartInteractive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"attachable"),o}tryStartExclusive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"exclusive"),o}async acquireInternal(e){const r=await this.gate.acquireInternal(e);return this.register(r,"internal","attachable"),r}bindRun(e,r){if(!e||!r)return;const o=this.states.get(e.operationId);o&&"recovery_required"!==o.phase&&(o.runId=r)}attachOrStartExternal(e,r,o){const t=this.stateForSession(e.sessionKey);if(t)return t.recovery?{ok:!1,reason:"session_busy",recovery:t.recovery}:"exclusive"===t.attachmentPolicy?{ok:!1,reason:"session_busy"}:t.runId&&o&&t.runId!==o?{ok:!1,reason:"run_conflict",recovery:this.fence(t,"run_conflict")}:(!t.runId&&o&&(t.runId=o),{ok:!0,ownership:"observer",ownerOperationId:t.token.operationId,attached:!0});const n=this.tryStartInteractive(e,r);return n.ok?(o&&this.bindRun(n.token,o),{ok:!0,ownership:"owner",token:n.token,attached:!1}):{ok:!1,reason:n.reason}}requestCancel(e,r){const o=this.requireState(e);if(o.recovery)return Promise.resolve({ok:!1,recovery:o.recovery});if(o.cancelPromise)return o.cancelPromise;if(o.terminalKind||o.convergencePromise)return Promise.resolve({ok:!1,reason:"terminal_already_observed"});o.phase="cancelling",o.cancelObservationPromise=new Promise(e=>{o.resolveCancelObservation=e});const t=o.cancelObservationPromise;o.cancelObservationTimer=this.setTimer(()=>{o.terminalKind||o.recovery||this.fence(o,"terminal_not_observed")},this.cancellationTerminalObservationTimeoutMs);const n=Promise.resolve().then(()=>r.chatAbort(e.sessionKey,o.runId)).then(r=>(r.aborted?(o.cancelAcknowledged=!0,this.logger.debug?.(`[session-mutation] remote abort acknowledged sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`)):this.logger.debug?.(`[session-mutation] remote abort not acknowledged; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:r.aborted,terminalObservation:t})).catch(r=>(this.logger.error?.(`[session-mutation] remote abort transport failed; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} error=${r instanceof Error?r.message:String(r)}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:!1,terminalObservation:t}));return o.cancelPromise=n,n}requestCancelForSession(e,r){const o=this.stateForSession(e);return o?"exclusive"===o.attachmentPolicy?Promise.resolve({ok:!1,reason:"not_cancellable"}):this.requestCancel(o.token,r):Promise.resolve({ok:!1,reason:"no_active_mutation"})}converge(e,r){const o=this.requireState(e);return o.convergencePromise||(o.terminalKind=r,this.resolveTerminalObserved(o),o.convergencePromise=this.proveConvergence(o)),o.convergencePromise}release(e){const r=this.states.get(e.operationId);r&&r.token.sessionKey===e.sessionKey&&"ready_to_release"===r.phase&&(this.clearCancellationTimer(r),this.gate.release(r.token),this.states.delete(e.operationId))}releaseBeforeRuntime(e){if(!e)return;const r=this.states.get(e.operationId);r?.recovery||(r&&this.clearCancellationTimer(r),this.gate.release(e),this.states.delete(e.operationId))}requireRecovery(e,r,o){const t=this.requireState(e);return o&&(t.terminalKind=o),this.fence(t,r)}snapshot(e){for(const r of this.states.values())if(r.token.sessionKey===e)return{sessionKey:e,operationId:r.token.operationId,surface:r.surface,attachmentPolicy:r.attachmentPolicy,phase:r.phase,...r.runId?{runId:r.runId}:{},...r.terminalKind?{terminalKind:r.terminalKind}:{},cancelAcknowledged:r.cancelAcknowledged,...r.recovery?{recovery:r.recovery}:{}}}gateSnapshot(e){return this.gate.snapshot(e)}fenceActive(e){for(const r of this.states.values())r.recovery||this.fence(r,e)}register(e,r,o){this.states.set(e.operationId,{token:e,surface:r,attachmentPolicy:o,phase:"active",cancelAcknowledged:!1})}requireState(e){const r=this.states.get(e.operationId);if(!r||r.token.sessionKey!==e.sessionKey)throw new Error("session_mutation_not_registered");return r}stateForSession(e){for(const r of this.states.values())if(r.token.sessionKey===e)return r}async proveConvergence(e){if(e.recovery)return{ok:!1,recovery:e.recovery};if(!e.terminalKind)return{ok:!1,recovery:this.fence(e,"terminal_not_observed")};if(e.recovery)return{ok:!1,recovery:e.recovery};e.phase="awaiting_quiescence";const r="cancelled"===e.terminalKind?this.cancellationQuiescenceTimeoutMs:this.observationTimeoutMs;let o;try{o=await this.quiescence.waitForQuiescence(e.token.sessionKey,r)}catch{return e.recovery?{ok:!1,recovery:e.recovery}:{ok:!1,recovery:this.fence(e,"quiescence_failed")}}if(e.recovery)return{ok:!1,recovery:e.recovery};if(!o.quiescent){const r="session_unresolved"===o.reason?"session_unresolved":"quiescence_timeout";return{ok:!1,recovery:this.fence(e,r,o)}}return e.phase="ready_to_release",this.logger.debug?.(`[session-mutation] convergence proven sessionKey=${e.token.sessionKey} operationId=${e.token.operationId} terminal=${e.terminalKind} elapsed_ms=${o.elapsedMs}`),{ok:!0,permit:{sessionKey:e.token.sessionKey,operationId:e.token.operationId}}}fence(e,r,o){if(e.recovery)return e.recovery;const t={code:"session_recovery_required",sessionKey:e.token.sessionKey,operationId:e.token.operationId,reason:r,surface:e.surface,...e.terminalKind?{terminalKind:e.terminalKind}:{},...o?{quiescence:o}:{}};e.recovery=t,e.phase="recovery_required",this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!1,recovery:t}),e.resolveCancelObservation=void 0,this.logger.error?.(`[session-mutation] recovery required sessionKey=${t.sessionKey} operationId=${t.operationId} reason=${t.reason} surface=${t.surface}`);try{this.onRecoveryRequired?.(t)}catch(e){this.logger.error?.(`[session-mutation] recovery restart scheduling failed sessionKey=${t.sessionKey} operationId=${t.operationId} error=${e instanceof Error?e.message:String(e)}`)}return t}resolveTerminalObserved(e){e.recovery||(this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!0,terminalObserved:!0}),e.resolveCancelObservation=void 0)}clearCancellationTimer(e){e.cancelObservationTimer&&(this.clearTimer(e.cancelObservationTimer),e.cancelObservationTimer=void 0)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RUNTIME_ERROR_DESCRIPTORS,RUNTIME_HASH_DOMAINS
|
|
1
|
+
import{RUNTIME_ERROR_DESCRIPTORS,RUNTIME_HASH_DOMAINS}from"@beeos-ai/beeos-types/runtime";import{verifyAndCreateMessageServiceCoreExecutionAttempt}from"./core-execution-attempt-context.js";import{RuntimeSessionNewRejectedError}from"./runtime-rpc-consumer.js";import{RUNTIME_MESSAGE_TYPES,runtimeMessageHash}from"./runtime-message-channel-contract.js";const SESSION_NEW_METHOD="session/new",SESSION_NEW_COMMAND="session.create";export class SessionNewRuntimeDeliveryBridge{options;workers=new Map;constructor(e){this.options=e}async acceptDelivery(e,t,s){this.currentLease(t);const r=record(e.message.payload),n=string(r?.operationId)??e.message.channel_id,o=this.workers.get(n);if(o)return await o;const i=this.handleDelivery(e,t,s).finally(()=>this.workers.delete(n));this.workers.set(n,i),await i}async handleDelivery(e,t,s){const r=parseSessionNewRequest(e);if(!r.ok)return void await this.options.poison(e,`invalid_session_new:${r.reason}`,t);const n=r.request,o=this.options.consumer(),i=async()=>{this.currentLease(t),await o.acknowledge([e.deliveryId])},a=await o.history(n.operationId);if("found"!==a.status)return void await i();const d=parseSessionNewHistory(a.snapshot);if(!d.ok||d.request.operationId!==n.operationId||d.request.requestMessageId!==n.requestMessageId||d.request.requestHash!==n.requestHash)return void await this.options.poison(e,"session_new_history_mismatch",t);const c=d.request;if(d.terminal)return void await i();if(c.instanceId!==t.instanceId||c.runtimeHandlerIdentity!==t.handlerIdentity||c.dispatchEpoch!==t.runtimeEpoch)return await this.appendFailure(c,t,"INSTANCE_EPOCH_MISMATCH"),void await i();const u=Date.now();if("deadline"===d.terminalSource||Date.parse(c.executionDeadlineAt)<=u||!d.started&&Date.parse(c.claimDeadlineAt)<=u)return await this.appendFailure(c,t,"COMMAND_EXPIRED"),void await i();if(d.cancelRequested&&!d.started)return await this.appendFailure(c,t,"OPERATION_CANCELLED"),void await i();const p=e.executionGrant;if(!p)return await this.appendFailure(c,t,d.started?"OUTCOME_UNKNOWN":"INVALID_EXECUTION_GRANT"),void await i();await this.options.verifier.refresh(),this.options.verifier.verifyAndDecodeGrant(p)||await this.options.verifier.refresh(!0);const h=new AbortController,l=()=>h.abort();s.addEventListener("abort",l,{once:!0});try{const e=await verifyAndCreateMessageServiceCoreExecutionAttempt({request:c,executionGrant:p,runtimeEpoch:t.runtimeEpoch,lease:t,verifier:this.options.verifier,allowExpiredGrant:d.started,abortSignal:h.signal,resolvePorts:async()=>({resources:noSessionNewResources})});if(!e.ok)return await this.appendFailure(c,t,d.started?"OUTCOME_UNKNOWN":"INVALID_EXECUTION_GRANT"),void await i();if(s.aborted)throw new Error("session/new delivery owner stopped");d.started?this.currentLease(t):await this.append(c,t,RUNTIME_MESSAGE_TYPES.started,{...correlation(c),executionGrantHash:c.executionGrantHash,handlerIdentity:t.handlerIdentity,executionMode:"start",claimedAt:(new Date).toISOString()},p);const r=c.request.params.agentId;let n;try{n=await this.options.createRuntimeSession({operationId:c.operationId,platformAgentId:r,...optionalStringProperty(c.request.params,"cwd"),...optionalStringProperty(c.command.params,"title")},e.context)}catch(e){if(!(e instanceof RuntimeSessionNewRejectedError))throw e;return await this.appendFailure(c,t,"INVALID_INVOCATION"),void await i()}const o=c.instanceId,a=r.startsWith("agent:")?r:`agent:${r}`;let u;try{u=await this.options.client.conversations.create({agentId:r,participants:dedupe([o,a,c.callerIdentityId]),...optionalStringProperty(c.command.params,"title"),metadata:{source:"session/new",runtimeSessionId:n.sessionId,runtimeSessionKey:n.gatewaySessionKey,runtimeInstanceId:c.instanceId,runtimeEpoch:c.dispatchEpoch,platformAgentId:r},idempotencyKey:c.operationId})}catch(e){const s=httpStatus(e);if(void 0===s||s>=500||408===s||429===s)throw e;return await this.appendFailure(c,t,"INVALID_INVOCATION"),void await i()}const l={sessionId:n.sessionId,conversationId:u.id},I=runtimeMessageHash(RUNTIME_HASH_DOMAINS.serviceResult,{method:"session/new",result:l});await this.append(c,t,RUNTIME_MESSAGE_TYPES.final,{...correlation(c),outcome:"succeeded",response:{jsonrpc:"2.0",id:c.request.id,result:l},serviceResultHash:I,completedAt:(new Date).toISOString()}),await i()}finally{h.abort(),s.removeEventListener("abort",l)}}async appendFailure(e,t,s){const r="COMMAND_EXPIRED"===s?"expired":"OPERATION_CANCELLED"===s?"cancelled":"OUTCOME_UNKNOWN"===s||"INSTANCE_EPOCH_MISMATCH"===s?"outcome_unknown":"failed",n="outcome_unknown"===r?"OUTCOME_UNKNOWN":s,o=RUNTIME_ERROR_DESCRIPTORS[n];await this.append(e,t,RUNTIME_MESSAGE_TYPES.final,{...correlation(e),outcome:r,response:{jsonrpc:"2.0",id:e.request.id,error:{code:o.jsonRpcCode,message:n,data:{code:n,operationId:e.operationId,effectState:r,retryMode:o.retryMode}}},completedAt:(new Date).toISOString()})}async append(e,t,s,r,n){this.currentLease(t);const o=this.options.consumer(),i=await o.append(e.operationId,s,r,n);if("created"===i.outcome||"duplicate"===i.outcome)return;let a=i.history;if(void 0===a){const t=await o.history(e.operationId);"found"===t.status&&(a=t.snapshot)}if(!hasCorrelatedMessage(a,s,e))throw new Error(`session/new ${s} append outcome is unknown`)}currentLease(e){const t=this.options.getLease();if(!t||t.instanceId!==e.instanceId||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new Error("session/new runtime delivery lease fence changed");if(!Number.isFinite(Date.parse(t.leaseExpiresAt))||Date.parse(t.leaseExpiresAt)<=Date.now())throw new Error("session/new runtime delivery lease expired");return t}}export function isCanonicalSessionNewDelivery(e){return"session/new"===record(e.message.payload)?.method}function parseSessionNewRequest(e){const t=e.message,s=record(t.payload),r=record(s?.request),n=record(r?.params),o=record(s?.command),i=record(o?.context),a=void 0===o?.params?void 0:record(o.params),d=[s?.operationId,s?.requestMessageId,s?.requestHash,s?.executionGrantHash,s?.instanceId,s?.runtimeHandlerIdentity,s?.dispatchEpoch,s?.callerIdentityId,s?.claimDeadlineAt,s?.executionDeadlineAt];return 1===s?.schemaVersion&&"session/new"===s?.method&&d.every(nonempty)&&"2.0"===r?.jsonrpc&&"session/new"===r.method&&("string"==typeof r.id||"number"==typeof r.id)&&n&&nonempty(n.agentId)&&"session.create"===o?.name&&i&&"instance"===i.scope&&i.operationId===s.operationId&&i.runtimeInstanceUid===s.instanceId&&i.dispatchEpoch===s.dispatchEpoch&&(void 0===o.params||a)&&t.type===RUNTIME_MESSAGE_TYPES.request&&t.channel_id===s.operationId&&t.message_id===s.requestMessageId&&Number.isFinite(Date.parse(s.claimDeadlineAt))&&Number.isFinite(Date.parse(s.executionDeadlineAt))?{ok:!0,request:s}:{ok:!1,reason:"required session/new fields do not correlate"}}function parseSessionNewHistory(e){const t=record(e),s=parseSessionNewRequest({deliveryId:"retained",redelivered:!0,idleMs:0,message:record(t?.request)});if(!s.ok||t?.operationId!==s.request.operationId||t.requestMessageId!==s.request.requestMessageId||t.requestHash!==s.request.requestHash)return{ok:!1};const r=record(t.started),n=record(r?.payload),o=void 0!==r&&n?.operationId===s.request.operationId&&n.requestMessageId===s.request.requestMessageId&&n.requestHash===s.request.requestHash&&n.dispatchEpoch===s.request.dispatchEpoch&&"session/new"===n.method;return{ok:!0,request:s.request,terminal:!0===t.terminal,terminalSource:string(t.terminalSource)??"none",cancelRequested:!0===t.cancelRequested,started:o}}function correlation(e){return{schemaVersion:1,operationId:e.operationId,requestMessageId:e.requestMessageId,requestHash:e.requestHash,dispatchEpoch:e.dispatchEpoch,method:"session/new"}}function hasCorrelatedMessage(e,t,s){const r=record(e);return(Array.isArray(r?.messages)?r.messages:[]).some(e=>{const r=record(e),n=record(r?.payload);return r?.type===t&&n?.operationId===s.operationId&&n.requestMessageId===s.requestMessageId&&n.requestHash===s.requestHash&&n.dispatchEpoch===s.dispatchEpoch&&"session/new"===n.method})}function optionalStringProperty(e,t){const s=e?.[t];return"string"==typeof s&&s.length>0?{[t]:s}:{}}function dedupe(e){return[...new Set(e.filter(e=>e.length>0))]}function httpStatus(e){const t=record(e),s=t?.status;return"number"==typeof s?s:void 0}function record(e){return null===e||"object"!=typeof e||Array.isArray(e)?void 0:e}function string(e){return"string"==typeof e&&e.length>0?e:void 0}function nonempty(e){return"string"==typeof e&&e.length>0}const noSessionNewResources=Object.freeze({async readContent(){throw new Error("session/new does not accept content resources")},async readVerifiedArtifact(){throw new Error("session/new does not accept artifact resources")}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function sessionSetModelMappingMatches(e,
|
|
1
|
+
export function sessionSetModelMappingMatches(e,s){if("session/set_reasoning"===e.method||"session.setReasoning"===s.name)return!("session/set_reasoning"!==e.method||"session.setReasoning"!==s.name||!record(e.params)||!Object.prototype.hasOwnProperty.call(e.params,"reasoningOverrideId"))&&(s.params.modelId===e.params.modelId&&s.params.reasoningOverrideId===e.params.reasoningOverrideId);if(!("session/set_model"===e.method||"session.setModel"===s.name))return!0;if("session/set_model"!==e.method||"session.setModel"!==s.name||!record(e.params)||!Object.prototype.hasOwnProperty.call(e.params,"modelOverrideId"))return!1;const r=e.params.modelOverrideId;return(null===r||"string"==typeof r&&0!==r.length&&r.trim()===r)&&s.params.modelOverrideId===r}function record(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const GATEWAY_GRANT_PROVENANCE=Object.freeze({issuer:"beeos-gateway",audience:"beeos-runtime-command",purpose:"runtime-execution-grant",maxClaimTtlSeconds:60}),MESSAGE_BOUNDARY_PROVENANCE=Object.freeze({issuer:"message-service",audience:"beeos-runtime-execution-boundary",purpose:"runtime-execution-boundary",maxClaimTtlSeconds:60});export class GatewayGrantJwksVerifier{verifier;purpose="gateway-runtime-grant";keysetProvenance;constructor(e){this.verifier=e,this.keysetProvenance=e.keysetUrl}async refresh(e=!1){await this.verifier.refresh(e),assertResponseProvenance(this.verifier,GATEWAY_GRANT_PROVENANCE)}trustedKeyMaterials(){return this.verifier.trustedKeyMaterials()}responseProvenance(){return this.verifier.documentProvenance()}verifyAndDecodeGrant(e){const r=this.verifier.verifyAndDecodeGrant(e);return"beeos-gateway"===r?.claims.iss&&"beeos-runtime-command"===r.claims.aud&&"runtime-execution-grant"===r.claims.purpose?r:null}}export class MessageServiceBoundaryJwksVerifier{verifier;purpose="message-service-execution-boundary";keysetProvenance;constructor(e){this.verifier=e,this.keysetProvenance=e.keysetUrl}async refresh(e=!1){await this.verifier.refresh(e),assertResponseProvenance(this.verifier,MESSAGE_BOUNDARY_PROVENANCE)}trustedKeyMaterials(){return this.verifier.trustedKeyMaterials()}responseProvenance(){return this.verifier.documentProvenance()}verifyAndDecodeBoundary(e){const r=this.verifier.verifyAndDecodeBoundary(e);return"message-service"===r?.claims.iss&&"beeos-runtime-execution-boundary"===r.claims.aud?r:null}}export class SplitIssuerRuntimeAuthorizationVerifier{grant;boundary;constructor(e,r){if(this.grant=e,this.boundary=r,e.keysetProvenance===r.keysetProvenance)throw new Error("grant and boundary JWKS keysets require distinct provenance")}async refresh(e=!1){await Promise.all([this.grant.refresh(e),this.boundary.refresh(e)]);const r=this.grant.responseProvenance(),s=this.boundary.responseProvenance();if(!r||!s||r.sourceUrl===s.sourceUrl||r.etag===s.etag)throw new Error("grant and boundary JWKS responses require distinct provenance");const n=new Set(this.boundary.trustedKeyMaterials().flatMap(e=>[e.jwkThumbprint,e.spkiSha256]));if(this.grant.trustedKeyMaterials().some(e=>n.has(e.jwkThumbprint)||n.has(e.spkiSha256)))throw new Error("grant and boundary JWKS keysets reuse public-key material")}verifyAndDecodeGrant(e){return this.grant.verifyAndDecodeGrant(e)}verifyAndDecodeBoundary(e){return this.boundary.verifyAndDecodeBoundary(e)}}function assertResponseProvenance(e,r){const s=e.documentProvenance();if(!s||!s.etag||s.sourceUrl!==e.keysetUrl||s.issuer!==r.issuer||s.audience!==r.audience||s.purpose!==r.purpose||s.maxClaimTtlSeconds!==r.maxClaimTtlSeconds)throw new Error(`JWKS response provenance mismatch for ${r.purpose}`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toMessagePart}from"../message-client-types.js";import{createRuntimeErrorPart,parseRuntimeErrorPart}from"
|
|
1
|
+
import{toMessagePart}from"../message-client-types.js";import{createRuntimeErrorPart,parseRuntimeErrorPart}from"@beeos-ai/message-sdk/protocol";export async function writeRuntimeFailure(r,e){if(e.businessErrorCode){r.parts.some(r=>parseRuntimeErrorPart(r))||r.addPart(toMessagePart(createRuntimeErrorPart(e.businessErrorCode)))}r.setBody(e.body),await r.fail({stopReason:e.stopReason})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TERMINAL_MAX_FRAME_BYTES,TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5,DEFAULT_LEASE_RECOVERY_GRACE_MS=3e5;export class RuntimeTerminalOutboundRegistration{options;service;authorityAdmission;serviceGeneration;rings=new Map;ringBytes=new Map;droppedThrough=new Map;pending=[];pendingBytes=0;activeSend;activeFlush;flushes=new Set;connected=!1;handlerIdentity;draining=!1;stopped=!1;eventGeneration=0;transportEpoch=0;drainPromise;closePromise;replayExpiryTimers=new Map;constructor(e){this.options=e,this.service=e.service??getOrCreateRuntimeTerminalService(e.manager),this.authorityAdmission=new TerminalAuthorityAdmission(e.authorityJournal,e.authorityClock),this.serviceGeneration=this.service.setOutbound(this);const t=this.eventGeneration;e.client.on("connected",(...i)=>{if(!this.acceptsEvent(t))return;if(this.connected)return;this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this);const n=i[0]?.handlerIdentity;n&&(this.handlerIdentity=n,this.service.closeParkedServiceSessions("runtime_lease_lost",n)),this.connected=!0,this.publishAvailability(!0),this.kickFlush(),e.logger?.info?.("[terminal-service] outbound registration connected")}),e.client.on("disconnected",()=>{this.acceptsEvent(t)&&this.invalidateConnection("transport")}),e.client.on("message",(...e)=>{if(!this.acceptsEvent(t))return;const i=this.serviceGeneration,n=this.transportEpoch;if(!i||!this.acceptsTransport(n,i))return;const r=e[0],s="string"==typeof r?r:Buffer.from(r).toString("utf8");this.handleWire(s,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",(...i)=>{if(this.acceptsEvent(t)){if(!0===i[0]?.recoverable)return this.invalidateConnection("park"),void e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals parked for recovery");this.invalidateConnection("fence"),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced")}}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection("park"),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch parked for resume"))})}async start(){if(this.stopped)throw new Error("terminal outbound registration is stopped");await this.options.client.connect()}isAvailable(){return this.connected&&!this.draining&&!this.stopped}send(e){if(this.stopped)return!1;const t=JSON.stringify(e),i=Buffer.byteLength(t);return this.remember(e,t,i),!this.connected||(this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:e,wire:t,bytes:i}),this.kickFlush(),!0)}resume(e,t,i){const n=this.transportEpoch;if(!this.acceptsConnection(n))throw new Error("terminal outbound registration is disconnected");const r=BigInt(i),s=this.droppedThrough.get(t)??0n,a=r<s;let o=[],c=-1n;for(const i of this.rings.get(t)??[]){if(i.frame.clientId!==e)throw new Error("terminal resume ownership mismatch");const n=BigInt(i.frame.sequence);if(n<=c)throw new Error("terminal replay sequence is not monotonic");c=n,n<=r||o.push({clientId:e,terminalId:t,frame:i.frame,wire:i.wire,bytes:i.bytes})}let h=a?s:void 0;if(0n===r||a){const e=this.options.maxResumeReplayOutputBytes??262144;let t=0,i=o.length;for(;i>0&&(t+=decodedOutputBytes(o[i-1].frame),!(t>e));)i--;i>0&&(h=BigInt(o[i-1].frame.sequence),o=o.slice(i))}if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");let d=0;for(const i of this.pending)i.terminalId!==t||i.clientId!==e?this.pending[d++]=i:this.pendingBytes-=i.bytes;if(this.pending.length=d,void 0!==h){const i={protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:e,terminalId:t,sequence:String(h),stream:"stdout",data:"",encoding:"base64",truncated:!0},n=JSON.stringify(i);this.enqueueWithOutputBackpressure({clientId:e,terminalId:t,frame:i,wire:n,bytes:Buffer.byteLength(n)})}for(const e of o)this.enqueueWithOutputBackpressure(e);return this.kickFlush(),{gap:a,truncated:void 0!==h}}rebindResumeClient(e,t,i){if(e===t)return;const n=this.rings.get(i);if(n?.some(t=>t.frame.clientId!==e))throw new Error("terminal resume ownership mismatch");if(n){let e=0;const r=n.map(i=>{const n=Object.freeze({...i.frame,clientId:t}),r=JSON.stringify(n),s=Buffer.byteLength(r);return e+=s,{frame:n,wire:r,bytes:s}});this.rings.set(i,r),this.ringBytes.set(i,e)}for(let n=0;n<this.pending.length;n++){const r=this.pending[n];if(r.terminalId!==i||r.clientId!==e)continue;const s={...r.frame,clientId:t},a=JSON.stringify(s),o=Buffer.byteLength(a);this.pendingBytes+=o-r.bytes,this.pending[n]={clientId:t,terminalId:i,frame:s,wire:a,bytes:o}}if(this.activeSend?.item.terminalId===i&&this.activeSend.item.clientId===e){const e=this.activeSend.item,n={...e.frame,clientId:t},r=JSON.stringify(n),s=Buffer.byteLength(r);this.activeSend.counted&&(this.pendingBytes+=s-e.bytes),this.activeSend={item:{clientId:t,terminalId:i,frame:n,wire:r,bytes:s},counted:this.activeSend.counted}}}forgetTerminal(e,t){this.prepareForgetTerminal(e,t)()}prepareForgetTerminal(e,t){const i=this.rings.get(t),n=new Set(this.pending.filter(e=>e.terminalId===t)),r=this.activeSend?.item.terminalId===t&&this.activeSend.counted?this.activeSend:void 0;if(i?.some(t=>t.frame.clientId!==e)||[...n].some(t=>t.clientId!==e)||r&&r.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const s=this.replayExpiryTimers.get(t);let a=!1;return()=>{if(a)return;a=!0,s&&this.replayExpiryTimers.get(t)===s&&(clearTimeout(s),this.replayExpiryTimers.delete(t)),i&&this.rings.get(t)===i&&(this.rings.delete(t),this.ringBytes.delete(t),this.droppedThrough.delete(t));let e=0;for(const t of this.pending)n.has(t)?this.pendingBytes-=t.bytes:this.pending[e++]=t;this.pending.length=e,r&&this.activeSend===r&&r.counted&&(r.counted=!1,this.pendingBytes-=r.item.bytes),this.pendingBytes=Math.max(0,this.pendingBytes)}}startTerminalReplayExpiry(e,t){if((this.rings.get(t)??[]).some(t=>t.frame.clientId!==e))throw new Error("terminal replay expiry ownership mismatch");this.scheduleReplayExpiry(e,t)}drain(e){return this.drainPromise??=this.drainOnce(e),this.drainPromise}async drainOnce(e){this.draining=!0,this.stopped=!0,this.eventGeneration++;const t=[...this.flushes];this.invalidateConnection("fence");const i=await waitBounded(this.options.manager.drain(e.timeoutMs,e.terminateAfterTimeout),e.timeoutMs,e.signal),n=i===interrupted?"terminated":i;return i!==interrupted&&"terminated"===n&&e.terminateAfterTimeout&&await waitBounded(this.options.manager.drain(Math.min(1e3,Math.max(50,e.timeoutMs)),!1),Math.min(1e3,Math.max(50,e.timeoutMs)),e.signal),await waitBounded(Promise.allSettled(t),e.timeoutMs,e.signal),await waitBounded(this.closeClientOnce(),e.timeoutMs,e.signal),this.clearReplayState(),n}acceptsEvent(e){return!this.stopped&&e===this.eventGeneration}async handleWire(e,t,i){if(!this.acceptsTransport(i,t))return;if(Buffer.byteLength(e)>TERMINAL_MAX_FRAME_BYTES)return void this.options.logger?.warn?.("[terminal-service] oversized client frame rejected");let n,r;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{r=await this.authorityAdmission.admit(n),await this.service.handleClientFrame(r.frame,t,r.admission)}catch(e){if(this.acceptsTransport(i,t)){r&&this.sendRejectedFrame(r.frame,e);const t=e instanceof Error?e.message:String(e);this.options.logger?.warn?.(`[terminal-service] client frame rejected: ${t}`,e)}}}sendRejectedFrame(e,t){const i=(t instanceof Error?t.message:String(t)).toLowerCase(),n=t instanceof TerminalNotFoundError||i.includes("not found")?"TERMINAL_NOT_FOUND":i.includes("quota")?"TERMINAL_QUOTA_EXCEEDED":i.includes("unauthorized")||i.includes("authority")?"UNAUTHORIZED_TARGET":"TERMINAL_IO_ERROR",r=this.rings.get(e.terminalId),s="TERMINAL_NOT_FOUND"!==n&&this.options.manager.getSession(e.terminalId)?.sessionId===e.clientId&&(!r||r.every(t=>t.frame.clientId===e.clientId)),a=s?this.options.manager.getSessionIncarnation(e.terminalId):void 0,o=s||"terminal.resume"!==e.type?a?BigInt(this.options.manager.nextServerSequence(e.terminalId,a)):1n:BigInt(e.lastServerSequence)+1n,c={protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.error",clientId:e.clientId,terminalId:e.terminalId,sequence:String(o),code:n,message:n,recoverable:"TERMINAL_IO_ERROR"===n||"TERMINAL_QUOTA_EXCEEDED"===n};if(!s){const t=JSON.stringify(c);return this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:c,wire:t,bytes:Buffer.byteLength(t)}),void this.kickFlush()}this.send(c)}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let r=(this.ringBytes.get(e.terminalId)??0)+i;const s=this.options.maxBufferedFramesPerTerminal??512,a=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>s||r>a;){const t=n.shift();if(!t)break;r-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,r),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}enqueueWithOutputBackpressure(e){const t=this.options.maxPendingSendBytes??1048576;if(this.pendingBytes+e.bytes<=t)return this.pending.push(e),void(this.pendingBytes+=e.bytes);if("terminal.output"===e.frame.type){if(this.hasPendingTruncation(e.terminalId))return;const i=this.toTruncationMarker(e);return this.compactPendingOutput(i.bytes,t),void(this.hasPendingTruncation(e.terminalId)||(this.pending.push(i),this.pendingBytes+=i.bytes))}this.compactPendingOutput(e.bytes,t),this.pending.push(e),this.pendingBytes+=e.bytes}compactPendingOutput(e,t){for(;this.pendingBytes+e>t;){const e=this.pending.findIndex(e=>"terminal.output"===e.frame.type&&!0!==e.frame.truncated);if(e<0)return;const t=this.pending[e];if(this.hasPendingTruncation(t.terminalId,t)){this.pending.splice(e,1),this.pendingBytes-=t.bytes;continue}const i=this.toTruncationMarker(t);this.pending[e]=i,this.pendingBytes+=i.bytes-t.bytes}}hasPendingTruncation(e,t){return this.activeSend?.item!==t&&this.activeSend?.item.terminalId===e&&"terminal.output"===this.activeSend.item.frame.type&&!0===this.activeSend.item.frame.truncated||this.pending.some(i=>i!==t&&i.terminalId===e&&"terminal.output"===i.frame.type&&!0===i.frame.truncated)}toTruncationMarker(e){if("terminal.output"!==e.frame.type)throw new Error("only terminal output can be truncated");const t={...e.frame,data:"",encoding:"base64",truncated:!0},i=JSON.stringify(t);return{...e,frame:t,wire:i,bytes:Buffer.byteLength(i)}}async flush(e){for(;this.acceptsConnection(e)&&this.pending.length>0;){const t=this.pending.shift(),i={item:t,counted:!0};this.activeSend=i;try{await this.options.client.send(t.wire)}catch{return void(this.transportEpoch===e&&this.invalidateConnection("transport"))}if(!this.acceptsConnection(e))return;this.activeSend===i&&(i.counted&&(this.pendingBytes-=t.bytes),this.activeSend=void 0)}}kickFlush(){if(this.activeFlush||!this.connected||0===this.pending.length)return;const e=this.transportEpoch,t=this.flush(e).finally(()=>{this.flushes.delete(t),this.activeFlush?.promise===t&&(this.activeFlush=void 0,this.acceptsConnection(e)&&this.kickFlush())});this.activeFlush={epoch:e,promise:t},this.flushes.add(t)}acceptsConnection(e){return this.connected&&!this.stopped&&e===this.transportEpoch}acceptsTransport(e,t){return this.acceptsEvent(this.eventGeneration)&&this.acceptsConnection(e)&&this.serviceGeneration===t}invalidateConnection(e){const t=this.connected;this.transportEpoch++,this.connected=!1,t&&this.publishAvailability(!1),this.activeFlush=void 0,this.activeSend=void 0,this.flushes.clear(),this.pending.length=0,this.pendingBytes=0;const i=this.serviceGeneration;switch(e){case"transport":return;case"park":i&&this.service.parkServiceSessions(i,this.handlerIdentity,this.options.leaseRecoveryGraceMs??3e5);break;case"fence":this.service.closeParkedServiceSessions("runtime_lease_lost"),i&&this.service.fenceServiceSessions("runtime_lease_lost",i);break;default:throw new Error(`unhandled connection invalidation ${String(e)}`)}i&&(this.service.clearOutbound(this,i),this.serviceGeneration===i&&(this.serviceGeneration=void 0)),this.clearReplayState()}publishAvailability(e){try{this.options.onAvailabilityChange?.(e)}catch{}}clearReplayState(){for(const e of this.replayExpiryTimers.values())clearTimeout(e);this.replayExpiryTimers.clear(),this.rings.clear(),this.ringBytes.clear(),this.droppedThrough.clear()}scheduleReplayExpiry(e,t){this.clearReplayExpiry(t);const i=this.serviceGeneration;if(!i)return;const n=setTimeout(()=>{if(this.replayExpiryTimers.get(t)===n){this.replayExpiryTimers.delete(t);try{this.service.expireServiceTerminalReplay(e,t,i)}catch(e){this.options.logger?.warn?.("[terminal-service] replay expiry failed",e)}}},Math.max(0,this.options.replayRetentionMs??3e5));n.unref?.(),this.replayExpiryTimers.set(t,n)}clearReplayExpiry(e){const t=this.replayExpiryTimers.get(e);t&&clearTimeout(t),this.replayExpiryTimers.delete(e)}closeClientOnce(){return this.closePromise??=this.options.client.close(!0),this.closePromise}}function decodedOutputBytes(e){return"terminal.output"===e.type?Buffer.byteLength(e.data,e.encoding):0}const interrupted=Symbol("interrupted");async function waitBounded(e,t,i){return i?.aborted?(e.catch(()=>{}),interrupted):await new Promise((n,r)=>{let s=!1;const a=e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),n(e))},o=()=>a(interrupted),c=setTimeout(()=>a(interrupted),Math.max(0,t));c.unref?.(),i?.addEventListener("abort",o,{once:!0}),e.then(e=>a(e),e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),r(e))})})}
|
|
1
|
+
import{TERMINAL_MAX_FRAME_BYTES,TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5;export class RuntimeTerminalOutboundRegistration{options;service;authorityAdmission;serviceGeneration;rings=new Map;ringBytes=new Map;droppedThrough=new Map;pending=[];pendingBytes=0;activeSend;activeFlush;flushes=new Set;connected=!1;draining=!1;stopped=!1;eventGeneration=0;transportEpoch=0;drainPromise;closePromise;replayExpiryTimers=new Map;constructor(e){this.options=e,this.service=e.service??getOrCreateRuntimeTerminalService(e.manager),this.authorityAdmission=new TerminalAuthorityAdmission(e.authorityJournal,e.authorityClock),this.serviceGeneration=this.service.setOutbound(this);const t=this.eventGeneration;e.client.on("connected",()=>{this.acceptsEvent(t)&&(this.connected||(this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this),this.connected=!0,this.publishAvailability(!0),this.kickFlush(),e.logger?.info?.("[terminal-service] outbound registration connected")))}),e.client.on("disconnected",()=>{this.acceptsEvent(t)&&this.invalidateConnection(!1)}),e.client.on("message",(...e)=>{if(!this.acceptsEvent(t))return;const i=this.serviceGeneration,n=this.transportEpoch;if(!i||!this.acceptsTransport(n,i))return;const s=e[0],r="string"==typeof s?s:Buffer.from(s).toString("utf8");this.handleWire(r,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced"))}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch fenced"))})}async start(){if(this.stopped)throw new Error("terminal outbound registration is stopped");await this.options.client.connect()}isAvailable(){return this.connected&&!this.draining&&!this.stopped}send(e){if(this.stopped)return!1;const t=JSON.stringify(e),i=Buffer.byteLength(t);return this.remember(e,t,i),!this.connected||(this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:e,wire:t,bytes:i}),this.kickFlush(),!0)}resume(e,t,i){const n=this.transportEpoch;if(!this.acceptsConnection(n))throw new Error("terminal outbound registration is disconnected");const s=BigInt(i),r=s<(this.droppedThrough.get(t)??0n),a=[];let o=-1n;for(const i of this.rings.get(t)??[]){if(i.frame.clientId!==e)throw new Error("terminal resume ownership mismatch");const n=BigInt(i.frame.sequence);if(n<=o)throw new Error("terminal replay sequence is not monotonic");o=n,n<=s||a.push({clientId:e,terminalId:t,frame:i.frame,wire:i.wire,bytes:i.bytes})}if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");for(const e of a)this.enqueueWithOutputBackpressure(e);return this.acknowledgeReplay(t,s),this.kickFlush(),{gap:r}}rebindResumeClient(e,t,i){if(e===t)return;const n=this.rings.get(i);if(n?.some(t=>t.frame.clientId!==e))throw new Error("terminal resume ownership mismatch");if(n){let e=0;const s=n.map(i=>{const n=Object.freeze({...i.frame,clientId:t}),s=JSON.stringify(n),r=Buffer.byteLength(s);return e+=r,{frame:n,wire:s,bytes:r}});this.rings.set(i,s),this.ringBytes.set(i,e)}for(let n=0;n<this.pending.length;n++){const s=this.pending[n];if(s.terminalId!==i||s.clientId!==e)continue;const r={...s.frame,clientId:t},a=JSON.stringify(r),o=Buffer.byteLength(a);this.pendingBytes+=o-s.bytes,this.pending[n]={clientId:t,terminalId:i,frame:r,wire:a,bytes:o}}if(this.activeSend?.item.terminalId===i&&this.activeSend.item.clientId===e){const e=this.activeSend.item,n={...e.frame,clientId:t},s=JSON.stringify(n),r=Buffer.byteLength(s);this.activeSend.counted&&(this.pendingBytes+=r-e.bytes),this.activeSend={item:{clientId:t,terminalId:i,frame:n,wire:s,bytes:r},counted:this.activeSend.counted}}}forgetTerminal(e,t){this.prepareForgetTerminal(e,t)()}prepareForgetTerminal(e,t){const i=this.rings.get(t),n=new Set(this.pending.filter(e=>e.terminalId===t)),s=this.activeSend?.item.terminalId===t&&this.activeSend.counted?this.activeSend:void 0;if(i?.some(t=>t.frame.clientId!==e)||[...n].some(t=>t.clientId!==e)||s&&s.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const r=this.replayExpiryTimers.get(t);let a=!1;return()=>{if(a)return;a=!0,r&&this.replayExpiryTimers.get(t)===r&&(clearTimeout(r),this.replayExpiryTimers.delete(t)),i&&this.rings.get(t)===i&&(this.rings.delete(t),this.ringBytes.delete(t),this.droppedThrough.delete(t));let e=0;for(const t of this.pending)n.has(t)?this.pendingBytes-=t.bytes:this.pending[e++]=t;this.pending.length=e,s&&this.activeSend===s&&s.counted&&(s.counted=!1,this.pendingBytes-=s.item.bytes),this.pendingBytes=Math.max(0,this.pendingBytes)}}startTerminalReplayExpiry(e,t){if((this.rings.get(t)??[]).some(t=>t.frame.clientId!==e))throw new Error("terminal replay expiry ownership mismatch");this.scheduleReplayExpiry(e,t)}drain(e){return this.drainPromise??=this.drainOnce(e),this.drainPromise}async drainOnce(e){this.draining=!0,this.stopped=!0,this.eventGeneration++;const t=[...this.flushes];this.invalidateConnection(!0);const i=await waitBounded(this.options.manager.drain(e.timeoutMs,e.terminateAfterTimeout),e.timeoutMs,e.signal),n=i===interrupted?"terminated":i;return i!==interrupted&&"terminated"===n&&e.terminateAfterTimeout&&await waitBounded(this.options.manager.drain(Math.min(1e3,Math.max(50,e.timeoutMs)),!1),Math.min(1e3,Math.max(50,e.timeoutMs)),e.signal),await waitBounded(Promise.allSettled(t),e.timeoutMs,e.signal),await waitBounded(this.closeClientOnce(),e.timeoutMs,e.signal),this.clearReplayState(),n}acceptsEvent(e){return!this.stopped&&e===this.eventGeneration}async handleWire(e,t,i){if(!this.acceptsTransport(i,t))return;if(Buffer.byteLength(e)>TERMINAL_MAX_FRAME_BYTES)return void this.options.logger?.warn?.("[terminal-service] oversized client frame rejected");let n,s;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{s=await this.authorityAdmission.admit(n),await this.service.handleClientFrame(s.frame,t,s.admission)}catch(e){if(this.acceptsTransport(i,t)){s&&this.sendRejectedFrame(s.frame,e);const t=e instanceof Error?e.message:String(e);this.options.logger?.warn?.(`[terminal-service] client frame rejected: ${t}`,e)}}}sendRejectedFrame(e,t){const i=(t instanceof Error?t.message:String(t)).toLowerCase(),n=t instanceof TerminalNotFoundError||i.includes("not found")?"TERMINAL_NOT_FOUND":i.includes("quota")?"TERMINAL_QUOTA_EXCEEDED":i.includes("unauthorized")||i.includes("authority")?"UNAUTHORIZED_TARGET":"TERMINAL_IO_ERROR",s=this.rings.get(e.terminalId),r="TERMINAL_NOT_FOUND"!==n&&this.options.manager.getSession(e.terminalId)?.sessionId===e.clientId&&(!s||s.every(t=>t.frame.clientId===e.clientId)),a=r?this.options.manager.getSessionIncarnation(e.terminalId):void 0,o=r||"terminal.resume"!==e.type?a?BigInt(this.options.manager.nextServerSequence(e.terminalId,a)):1n:BigInt(e.lastServerSequence)+1n,c={protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.error",clientId:e.clientId,terminalId:e.terminalId,sequence:String(o),code:n,message:n,recoverable:"TERMINAL_IO_ERROR"===n||"TERMINAL_QUOTA_EXCEEDED"===n};if(!r){const t=JSON.stringify(c);return this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:c,wire:t,bytes:Buffer.byteLength(t)}),void this.kickFlush()}this.send(c)}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let s=(this.ringBytes.get(e.terminalId)??0)+i;const r=this.options.maxBufferedFramesPerTerminal??512,a=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>r||s>a;){const t=n.shift();if(!t)break;s-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,s),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}enqueueWithOutputBackpressure(e){const t=this.options.maxPendingSendBytes??1048576;if(this.pendingBytes+e.bytes<=t)return this.pending.push(e),void(this.pendingBytes+=e.bytes);if("terminal.output"===e.frame.type){if(this.hasPendingTruncation(e.terminalId))return;const i=this.toTruncationMarker(e);return this.compactPendingOutput(i.bytes,t),void(this.hasPendingTruncation(e.terminalId)||(this.pending.push(i),this.pendingBytes+=i.bytes))}this.compactPendingOutput(e.bytes,t),this.pending.push(e),this.pendingBytes+=e.bytes}compactPendingOutput(e,t){for(;this.pendingBytes+e>t;){const e=this.pending.findIndex(e=>"terminal.output"===e.frame.type&&!0!==e.frame.truncated);if(e<0)return;const t=this.pending[e];if(this.hasPendingTruncation(t.terminalId,t)){this.pending.splice(e,1),this.pendingBytes-=t.bytes;continue}const i=this.toTruncationMarker(t);this.pending[e]=i,this.pendingBytes+=i.bytes-t.bytes}}hasPendingTruncation(e,t){return this.activeSend?.item!==t&&this.activeSend?.item.terminalId===e&&"terminal.output"===this.activeSend.item.frame.type&&!0===this.activeSend.item.frame.truncated||this.pending.some(i=>i!==t&&i.terminalId===e&&"terminal.output"===i.frame.type&&!0===i.frame.truncated)}toTruncationMarker(e){if("terminal.output"!==e.frame.type)throw new Error("only terminal output can be truncated");const t={...e.frame,data:"",encoding:"base64",truncated:!0},i=JSON.stringify(t);return{...e,frame:t,wire:i,bytes:Buffer.byteLength(i)}}async flush(e){for(;this.acceptsConnection(e)&&this.pending.length>0;){const t=this.pending.shift(),i={item:t,counted:!0};this.activeSend=i;try{await this.options.client.send(t.wire)}catch{return void(this.transportEpoch===e&&this.invalidateConnection(!1))}if(!this.acceptsConnection(e))return;this.activeSend===i&&(i.counted&&(this.pendingBytes-=t.bytes),this.activeSend=void 0)}}kickFlush(){if(this.activeFlush||!this.connected||0===this.pending.length)return;const e=this.transportEpoch,t=this.flush(e).finally(()=>{this.flushes.delete(t),this.activeFlush?.promise===t&&(this.activeFlush=void 0,this.acceptsConnection(e)&&this.kickFlush())});this.activeFlush={epoch:e,promise:t},this.flushes.add(t)}acceptsConnection(e){return this.connected&&!this.stopped&&e===this.transportEpoch}acceptsTransport(e,t){return this.acceptsEvent(this.eventGeneration)&&this.acceptsConnection(e)&&this.serviceGeneration===t}invalidateConnection(e){const t=this.connected;if(this.transportEpoch++,this.connected=!1,t&&this.publishAvailability(!1),this.activeFlush=void 0,this.activeSend=void 0,this.flushes.clear(),this.pending.length=0,this.pendingBytes=0,!e)return;const i=this.serviceGeneration;i&&(this.service.fenceServiceSessions("runtime_lease_lost",i),this.service.clearOutbound(this,i),this.serviceGeneration===i&&(this.serviceGeneration=void 0)),this.clearReplayState()}publishAvailability(e){try{this.options.onAvailabilityChange?.(e)}catch{}}clearReplayState(){for(const e of this.replayExpiryTimers.values())clearTimeout(e);this.replayExpiryTimers.clear(),this.rings.clear(),this.ringBytes.clear(),this.droppedThrough.clear()}acknowledgeReplay(e,t){const i=this.rings.get(e);if(!i||0===i.length)return;let n=0,s=this.droppedThrough.get(e)??0n,r=0;for(;r<i.length;){const e=i[r],a=BigInt(e.frame.sequence);if(a>t)break;n+=e.bytes,a>s&&(s=a),r++}0!==r&&(i.splice(0,r),this.ringBytes.set(e,Math.max(0,(this.ringBytes.get(e)??0)-n)),this.droppedThrough.set(e,s))}scheduleReplayExpiry(e,t){this.clearReplayExpiry(t);const i=this.serviceGeneration;if(!i)return;const n=setTimeout(()=>{if(this.replayExpiryTimers.get(t)===n){this.replayExpiryTimers.delete(t);try{this.service.expireServiceTerminalReplay(e,t,i)}catch(e){this.options.logger?.warn?.("[terminal-service] replay expiry failed",e)}}},Math.max(0,this.options.replayRetentionMs??3e5));n.unref?.(),this.replayExpiryTimers.set(t,n)}clearReplayExpiry(e){const t=this.replayExpiryTimers.get(e);t&&clearTimeout(t),this.replayExpiryTimers.delete(e)}closeClientOnce(){return this.closePromise??=this.options.client.close(!0),this.closePromise}}const interrupted=Symbol("interrupted");async function waitBounded(e,t,i){return i?.aborted?(e.catch(()=>{}),interrupted):await new Promise((n,s)=>{let r=!1;const a=e=>{r||(r=!0,clearTimeout(c),i?.removeEventListener("abort",o),n(e))},o=()=>a(interrupted),c=setTimeout(()=>a(interrupted),Math.max(0,t));c.unref?.(),i?.addEventListener("abort",o,{once:!0}),e.then(e=>a(e),e=>{r||(r=!0,clearTimeout(c),i?.removeEventListener("abort",o),s(e))})})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";const MAX_OUTPUT_FRAME_BYTES=16384,OUTPUT_FLUSH_INTERVAL_MS=16;export class RuntimeTerminalService{manager;clients=new Map;clientSequences=new Map;openingFrames=new Map;pendingOutput=new Map;parked=new Map;outboundGeneration;constructor(e,n){this.manager=e,n&&this.setOutbound(n),e.addListeners({onOutput:e=>{const n=this.clients.get(e.terminalId);"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.queueOutput(e.terminalId,e.stream,e.data,e.seq,n)},onExit:e=>{const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)===n&&(this.clients.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,n.incarnation))},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&(this.flushPendingOutput(e.terminalId),this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.exit",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),...null===e.code?{}:{exitCode:e.code},...null===e.signal?{}:{signal:e.signal}},n))},onStateChange:e=>{if("closing"!==e.to)return;const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)!==n||this.manager.isCurrentSession(e.terminalId,e.incarnation)||this.clients.delete(e.terminalId)},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.isOwnedTerminal(n)&&n.generation.outbound.startTerminalReplayExpiry(n.clientId,e.terminalId)}})}setOutbound(e){if(this.outboundGeneration?.outbound===e)return this.outboundGeneration;if(this.outboundGeneration)throw new Error("Terminal outbound sink is already configured");const n=Object.freeze({outbound:e});return this.outboundGeneration=n,n}clearOutbound(e,n){const t=n??this.outboundGeneration;t&&t.outbound===e&&this.outboundGeneration===t&&(this.outboundGeneration=void 0,this.clearGenerationOwnership(t))}fenceServiceSessions(e="runtime_lease_lost",n=this.outboundGeneration){for(const[t,i]of this.clients)"service"!==i.kind||n&&i.generation!==n||this.manager.closeSessionIfCurrent(t,i.incarnation,e);if(n)this.clearGenerationOwnership(n);else{for(const[e,n]of this.clients)"service"===n.kind&&this.clients.delete(e);this.clientSequences.clear();for(const[e,n]of this.openingFrames)"service"===n.owner.kind&&this.openingFrames.delete(e)}}parkServiceSessions(e,n,t){for(const[i,r]of this.clients){if("service"!==r.kind||r.generation!==e)continue;if("running"!==this.manager.getSession(i)?.state){this.manager.closeSessionIfCurrent(i,r.incarnation,"runtime_lease_lost");continue}const s=setTimeout(()=>{this.parked.get(i)?.timer===s&&this.closeParked(i,"runtime_lease_lost")},Math.max(0,t));s.unref?.(),this.parked.set(i,{clientId:r.clientId,incarnation:r.incarnation,handlerIdentity:n,timer:s})}this.clearGenerationOwnership(e)}closeParkedServiceSessions(e,n){for(const[t,i]of this.parked)void 0!==n&&i.handlerIdentity===n||this.closeParked(t,e)}async handleClientFrame(e,n=this.outboundGeneration,t){if(!n||!this.isGenerationActive(n))throw new Error("terminal outbound generation is inactive");assertTerminalClientFrame(e);const i=BigInt(e.clientSequence),r=this.clientSequences.get(e.clientId);if(i<=(r?.generation===n?r.sequence:0n))throw new Error("non-monotonic terminal client sequence");if(this.clientSequences.set(e.clientId,{generation:n,sequence:i}),"terminal.open"===e.type||"terminal.create"===e.type){if(t?.recovered&&!this.clients.has(e.terminalId)&&!this.manager.getSession(e.terminalId))throw new TerminalNotFoundError(e.terminalId);if(this.clients.has(e.terminalId)||this.manager.getSession(e.terminalId)){const i=this.clients.get(e.terminalId);if(t?.recovered&&"service"===i?.kind&&i.clientId===e.clientId&&i.generation===n&&this.isOwnedTerminal(i)){const n=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(i),resumed:!0,columns:n.cols,rows:n.rows},i)}throw new Error("terminal already exists; use terminal.resume")}const i=this.manager.createSessionIncarnation(e.terminalId),r={kind:"service",clientId:e.clientId,generation:n,incarnation:i};this.clients.set(e.terminalId,r),this.openingFrames.set(e.terminalId,{owner:r,frames:[]});try{const n=this.manager.openSession({terminalId:e.terminalId,sessionId:e.clientId,command:e.command,args:e.args,env:e.env,cwd:e.cwd,cols:"terminal.open"===e.type?e.columns??80:80,rows:"terminal.open"===e.type?e.rows??24:24,outputLimitBytes:e.outputLimitBytes,initialServerSequence:2,incarnation:i});if(!this.isOwnedTerminal(r))return void this.manager.releaseSessionIfCurrent(e.terminalId,i);this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:n.terminalId,sequence:"1",resumed:!0===t?.recovered,columns:n.cols,rows:n.rows},r);const s=this.openingFrames.get(e.terminalId);s?.owner===r&&this.openingFrames.delete(e.terminalId);for(const e of s?.owner===r?s.frames:[])this.sendOrClose(e,r)}catch(n){throw this.clients.get(e.terminalId)===r&&this.clients.delete(e.terminalId),this.openingFrames.get(e.terminalId)?.owner===r&&this.openingFrames.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,i),n}return}let s=!1;"terminal.resume"===e.type&&(s=this.adoptParkedForResume(e.clientId,e.terminalId,n,t),this.rebindServiceOwnerForResume(e.clientId,e.terminalId,n));const a=this.assertOwner(e.clientId,e.terminalId,n);switch(e.type){case"terminal.resume":{if(n.outbound.resume(e.clientId,e.terminalId,e.lastServerSequence),!this.isOwnedTerminal(a))return;if(s&&this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),stream:"stdout",data:"",encoding:"base64",truncated:!0},a),!this.isOwnedTerminal(a))return;const t=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),resumed:!0,columns:t.cols,rows:t.rows},a)}case"terminal.input":return void this.manager.writeToSessionIfCurrent(e.terminalId,a.incarnation,Buffer.from(e.data,"base64"===e.encoding?"base64":"utf8"));case"terminal.resize":return void this.manager.resizeSessionIfCurrent(e.terminalId,a.incarnation,e.columns,e.rows);case"terminal.close":if(!this.manager.closeSessionIfCurrent(e.terminalId,a.incarnation,e.reason))throw new TerminalNotFoundError(e.terminalId);return;case"terminal.ping":if(this.manager.touchSessionIfCurrent(e.terminalId,a.incarnation),!this.isOwnedTerminal(a))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.pong",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),nonce:e.nonce},a);case"terminal.wait":{let n;try{n=await this.manager.waitForExitIfCurrent(e.terminalId,a.incarnation,e.timeoutMs)}catch(e){if(!this.isOwnedTerminal(a))return;throw e}if(!this.isOwnedTerminal(a))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.wait_result",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),exitCode:n.exitCode,output:n.output,state:n.state},a)}case"terminal.release":if(!this.manager.isCurrentSession(e.terminalId,a.incarnation))throw new TerminalNotFoundError(e.terminalId);const t=n.outbound.prepareForgetTerminal(e.clientId,e.terminalId);if(!this.manager.detachSessionIfCurrent(e.terminalId,a.incarnation))throw new TerminalNotFoundError(e.terminalId);return this.clients.get(e.terminalId)===a&&this.clients.delete(e.terminalId),this.dropPendingOutput(e.terminalId),void t();case"terminal.kill":if(!this.manager.killSessionIfCurrent(e.terminalId,a.incarnation,normalizeSignal(e.signal)))throw new TerminalNotFoundError(e.terminalId);return}}async execute(e,n){const t=this.outboundGeneration,i=t?.outbound;if(i?.isAvailable&&!i.isAvailable())throw new Error("terminal Service transport is unavailable; legacy fallback is forbidden");const r=e,s=r.context.terminal.terminalId,a=r.context.terminal.clientId;switch(r.name){case"terminal.open":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:e.params.columns,rows:e.params.rows,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command??t.shell,shell:t.shell,cwd:t.cwd,columns:t.cols,rows:t.rows,state:t.state}}case"terminal.input":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.writeToSessionIfCurrent(s,n.incarnation,Buffer.from(e.params.data,"base64"===e.params.encoding?"base64":"utf8")),{accepted:!0}}case"terminal.resize":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.resizeSessionIfCurrent(s,n.incarnation,e.params.columns,e.params.rows),{resized:!0}}case"terminal.close":{const e=this.assertAcpOwner(a,s);if(!this.manager.closeSessionIfCurrent(s,e.incarnation,r.params.reason))throw new TerminalNotFoundError(s);return{closed:!0}}case"terminal.ping":{const e=this.assertAcpOwner(a,s);return this.manager.touchSessionIfCurrent(s,e.incarnation),{terminalId:s,status:"ok"}}case"terminal.create":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:80,rows:24,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command}}case"terminal.wait":{const e=r,n=this.assertAcpOwner(a,s),t=await this.manager.waitForExitIfCurrent(s,n.incarnation,e.params.timeoutMs);if(!this.isOwnedAcpTerminal(n))throw new TerminalNotFoundError(s);return t}case"terminal.release":{const e=this.assertAcpOwner(a,s);if(!this.manager.detachSessionIfCurrent(s,e.incarnation))throw new TerminalNotFoundError(s);return this.clients.get(s)===e&&this.clients.delete(s),{released:!0}}case"terminal.kill":{const e=this.assertAcpOwner(a,s);if(!this.manager.killSessionIfCurrent(s,e.incarnation,normalizeSignal(r.params.signal)))throw new TerminalNotFoundError(s);return{killed:!0}}}throw new Error(`unsupported terminal command ${r.name}`)}nextSequence(e){return String(this.manager.nextServerSequence(e.incarnation.terminalId,e.incarnation))}sendOrClose(e,n){this.isOwnedTerminal(n)&&!n.generation.outbound.send(e)&&"terminal.output"!==e.type&&this.isOwnedTerminal(n)&&this.manager.closeSessionIfCurrent(e.terminalId,n.incarnation,"backpressure_limit")}emitFrame(e,n){if(!this.isOwnedTerminal(n))return;const t=this.openingFrames.get(e.terminalId);t?.owner===n?t.frames.push(e):this.sendOrClose(e,n)}assertOwner(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))throw new TerminalNotFoundError(n);return i}rebindServiceOwnerForResume(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.generation!==t||!this.isOwnedTerminal(i))return;if(i.clientId===e)return;if(this.flushPendingOutput(n),!this.manager.rebindSessionId(n,i.incarnation,e))throw new TerminalNotFoundError(n);t.outbound.rebindResumeClient(i.clientId,e,n),this.clientSequences.delete(i.clientId);const r={kind:"service",clientId:e,generation:i.generation,incarnation:i.incarnation};this.clients.set(n,r)}adoptParkedForResume(e,n,t,i){const r=this.parked.get(n);return!(!r||!0!==i?.recovered||this.clients.has(n))&&(clearTimeout(r.timer),this.parked.delete(n),"running"!==this.manager.getSession(n)?.state||r.clientId!==e&&!this.manager.rebindSessionId(n,r.incarnation,e)?(this.retireParked(n,r,"runtime_lease_lost"),!1):(this.clients.set(n,{kind:"service",clientId:e,generation:t,incarnation:r.incarnation}),!0))}closeParked(e,n){const t=this.parked.get(e);t&&(clearTimeout(t.timer),this.parked.delete(e),this.retireParked(e,t,n))}retireParked(e,n,t){"closed"===this.manager.getSession(e)?.state?this.manager.releaseSessionIfCurrent(e,n.incarnation):this.manager.closeSessionIfCurrent(e,n.incarnation,t)}assertAcpOwner(e,n){const t=this.clients.get(n);if("acp"!==t?.kind||t.clientId!==e||!this.isOwnedAcpTerminal(t))throw new TerminalNotFoundError(n);return t}isGenerationActive(e){return this.outboundGeneration===e}isOwnedTerminal(e){return this.outboundGeneration===e.generation&&this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}isOwnedAcpTerminal(e){return this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}expireServiceTerminalReplay(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))return;const r=t.outbound.prepareForgetTerminal(e,n);if(!("closed"===this.manager.getSession(n)?.state?this.manager.releaseSessionIfCurrent(n,i.incarnation):this.manager.detachSessionIfCurrent(n,i.incarnation)))throw new TerminalNotFoundError(n);this.clients.get(n)===i&&this.clients.delete(n),this.openingFrames.get(n)?.owner===i&&this.openingFrames.delete(n),this.dropPendingOutput(n),r()}clearGenerationOwnership(e){for(const[n,t]of this.clients)"service"===t.kind&&t.generation===e&&this.clients.delete(n);for(const[n,t]of this.clientSequences)t.generation===e&&this.clientSequences.delete(n);for(const[n,t]of this.openingFrames)t.owner.generation===e&&this.openingFrames.delete(n);for(const[n,t]of this.pendingOutput)t.owner.generation===e&&this.dropPendingOutput(n)}queueOutput(e,n,t,i,r){let s=0,a=i;for(;s<t.length;){let i=this.pendingOutput.get(e);!i||i.owner===r&&i.stream===n||(this.flushPendingOutput(e),i=void 0),i||(i={owner:r,stream:n,chunks:[],bytes:0,sequence:a},this.pendingOutput.set(e,i));const o=Math.min(16384-i.bytes,t.length-s);i.chunks.push(t.subarray(s,s+o)),i.bytes+=o,i.sequence=a,s+=o,16384!==i.bytes?i.timer||(i.timer=setTimeout(()=>{this.pendingOutput.get(e)===i&&this.flushPendingOutput(e)},16),i.timer.unref?.()):(this.flushPendingOutput(e),s<t.length&&(a=this.manager.nextServerSequence(e,r.incarnation)))}}flushPendingOutput(e){const n=this.pendingOutput.get(e);if(!n)return;if(this.pendingOutput.delete(e),n.timer&&clearTimeout(n.timer),0===n.bytes||!this.isOwnedTerminal(n.owner))return;const t=1===n.chunks.length?n.chunks[0]:Buffer.concat(n.chunks,n.bytes);this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:n.owner.clientId,terminalId:e,sequence:String(n.sequence),stream:n.stream,data:t.toString("base64"),encoding:"base64"},n.owner)}dropPendingOutput(e){const n=this.pendingOutput.get(e);n&&(n.timer&&clearTimeout(n.timer),this.pendingOutput.delete(e))}}const runtimeServices=new WeakMap;export function getOrCreateRuntimeTerminalService(e){let n=runtimeServices.get(e);return n||(n=new RuntimeTerminalService(e),runtimeServices.set(e,n)),n}export function assertTerminalClientFrame(e){const n=e;if(!(n&&CLIENT_FRAME_TYPES.has(String(n.type))&&n.protocolVersion===TERMINAL_SERVICE_PROTOCOL_VERSION&&isUuid(n.clientId)&&isUuid(n.terminalId)&&isPositiveRuntimeSequence(n.clientSequence)))throw new Error("invalid terminal client frame");switch(e.type){case"terminal.open":assertOptionalCommand(e.command),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalDimension(e.columns),assertOptionalDimension(e.rows),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.create":("string"!=typeof e.command||e.command.length<1||e.command.length>4096)&&invalidFrame(),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.resume":isRuntimeSequence(e.lastServerSequence)||invalidFrame();break;case"terminal.input":("string"!=typeof e.data||"utf8"!==e.encoding&&"base64"!==e.encoding)&&invalidFrame();break;case"terminal.resize":assertDimension(e.columns),assertDimension(e.rows);break;case"terminal.close":assertOptionalString(e.reason);break;case"terminal.ping":"string"!=typeof e.nonce&&invalidFrame();break;case"terminal.wait":void 0!==e.timeoutMs&&(!Number.isInteger(e.timeoutMs)||e.timeoutMs<0||e.timeoutMs>864e5)&&invalidFrame();break;case"terminal.kill":assertOptionalString(e.signal)}}const CLIENT_FRAME_TYPES=new Set(["terminal.open","terminal.resume","terminal.input","terminal.resize","terminal.close","terminal.ping","terminal.create","terminal.wait","terminal.release","terminal.kill"]);function normalizeSignal(e){return e&&e.startsWith("SIG")?e:"SIGTERM"}const UUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,UINT64_MAX=18446744073709551615n;function isUuid(e){return"string"==typeof e&&UUID_PATTERN.test(e)}function isRuntimeSequence(e){return"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)<=UINT64_MAX}function isPositiveRuntimeSequence(e){return isRuntimeSequence(e)&&"0"!==e}function invalidFrame(){throw new Error("invalid terminal client frame")}function assertOptionalString(e){void 0!==e&&"string"!=typeof e&&invalidFrame()}function assertOptionalCommand(e){void 0!==e&&("string"!=typeof e||e.length<1||e.length>4096)&&invalidFrame()}function assertOptionalArgs(e){void 0===e||Array.isArray(e)&&e.every(e=>"string"==typeof e)||invalidFrame()}function assertOptionalEnv(e){void 0===e||e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>"string"==typeof e)||invalidFrame()}function assertDimension(e){(!Number.isInteger(e)||e<1||e>1e3)&&invalidFrame()}function assertOptionalDimension(e){void 0!==e&&assertDimension(e)}function assertOptionalOutputLimit(e){void 0!==e&&(!Number.isInteger(e)||e<1||e>16777216)&&invalidFrame()}
|
|
1
|
+
import{TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";const MAX_OUTPUT_FRAME_BYTES=16384,OUTPUT_FLUSH_INTERVAL_MS=16;export class RuntimeTerminalService{manager;clients=new Map;clientSequences=new Map;openingFrames=new Map;pendingOutput=new Map;outboundGeneration;constructor(e,n){this.manager=e,n&&this.setOutbound(n),e.addListeners({onOutput:e=>{const n=this.clients.get(e.terminalId);"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.queueOutput(e.terminalId,e.stream,e.data,e.seq,n)},onExit:e=>{const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)===n&&(this.clients.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,n.incarnation))},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&(this.flushPendingOutput(e.terminalId),this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.exit",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),...null===e.code?{}:{exitCode:e.code},...null===e.signal?{}:{signal:e.signal}},n))},onStateChange:e=>{if("closing"!==e.to)return;const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)!==n||this.manager.isCurrentSession(e.terminalId,e.incarnation)||this.clients.delete(e.terminalId)},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.isOwnedTerminal(n)&&n.generation.outbound.startTerminalReplayExpiry(n.clientId,e.terminalId)}})}setOutbound(e){if(this.outboundGeneration?.outbound===e)return this.outboundGeneration;if(this.outboundGeneration)throw new Error("Terminal outbound sink is already configured");const n=Object.freeze({outbound:e});return this.outboundGeneration=n,n}clearOutbound(e,n){const t=n??this.outboundGeneration;t&&t.outbound===e&&this.outboundGeneration===t&&(this.outboundGeneration=void 0,this.clearGenerationOwnership(t))}fenceServiceSessions(e="runtime_lease_lost",n=this.outboundGeneration){for(const[t,i]of this.clients)"service"!==i.kind||n&&i.generation!==n||this.manager.closeSessionIfCurrent(t,i.incarnation,e);if(n)this.clearGenerationOwnership(n);else{for(const[e,n]of this.clients)"service"===n.kind&&this.clients.delete(e);this.clientSequences.clear();for(const[e,n]of this.openingFrames)"service"===n.owner.kind&&this.openingFrames.delete(e)}}async handleClientFrame(e,n=this.outboundGeneration,t){if(!n||!this.isGenerationActive(n))throw new Error("terminal outbound generation is inactive");assertTerminalClientFrame(e);const i=BigInt(e.clientSequence),r=this.clientSequences.get(e.clientId);if(i<=(r?.generation===n?r.sequence:0n))throw new Error("non-monotonic terminal client sequence");if(this.clientSequences.set(e.clientId,{generation:n,sequence:i}),"terminal.open"===e.type||"terminal.create"===e.type){if(t?.recovered&&!this.clients.has(e.terminalId)&&!this.manager.getSession(e.terminalId))throw new TerminalNotFoundError(e.terminalId);if(this.clients.has(e.terminalId)||this.manager.getSession(e.terminalId)){const i=this.clients.get(e.terminalId);if(t?.recovered&&"service"===i?.kind&&i.clientId===e.clientId&&i.generation===n&&this.isOwnedTerminal(i)){const n=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(i),resumed:!0,columns:n.cols,rows:n.rows},i)}throw new Error("terminal already exists; use terminal.resume")}const i=this.manager.createSessionIncarnation(e.terminalId),r={kind:"service",clientId:e.clientId,generation:n,incarnation:i};this.clients.set(e.terminalId,r),this.openingFrames.set(e.terminalId,{owner:r,frames:[]});try{const n=this.manager.openSession({terminalId:e.terminalId,sessionId:e.clientId,command:e.command,args:e.args,env:e.env,cwd:e.cwd,cols:"terminal.open"===e.type?e.columns??80:80,rows:"terminal.open"===e.type?e.rows??24:24,outputLimitBytes:e.outputLimitBytes,initialServerSequence:2,incarnation:i});if(!this.isOwnedTerminal(r))return void this.manager.releaseSessionIfCurrent(e.terminalId,i);this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:n.terminalId,sequence:"1",resumed:!0===t?.recovered,columns:n.cols,rows:n.rows},r);const s=this.openingFrames.get(e.terminalId);s?.owner===r&&this.openingFrames.delete(e.terminalId);for(const e of s?.owner===r?s.frames:[])this.sendOrClose(e,r)}catch(n){throw this.clients.get(e.terminalId)===r&&this.clients.delete(e.terminalId),this.openingFrames.get(e.terminalId)?.owner===r&&this.openingFrames.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,i),n}return}"terminal.resume"===e.type&&this.rebindServiceOwnerForResume(e.clientId,e.terminalId,n);const s=this.assertOwner(e.clientId,e.terminalId,n);switch(e.type){case"terminal.resume":{const t=n.outbound.resume(e.clientId,e.terminalId,e.lastServerSequence);if(!this.isOwnedTerminal(s))return;if(t.gap&&this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),stream:"stdout",data:"",encoding:"base64",truncated:!0},s),!this.isOwnedTerminal(s))return;const i=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),resumed:!0,columns:i.cols,rows:i.rows},s)}case"terminal.input":return void this.manager.writeToSessionIfCurrent(e.terminalId,s.incarnation,Buffer.from(e.data,"base64"===e.encoding?"base64":"utf8"));case"terminal.resize":return void this.manager.resizeSessionIfCurrent(e.terminalId,s.incarnation,e.columns,e.rows);case"terminal.close":if(!this.manager.closeSessionIfCurrent(e.terminalId,s.incarnation,e.reason))throw new TerminalNotFoundError(e.terminalId);return;case"terminal.ping":if(this.manager.touchSessionIfCurrent(e.terminalId,s.incarnation),!this.isOwnedTerminal(s))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.pong",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),nonce:e.nonce},s);case"terminal.wait":{let n;try{n=await this.manager.waitForExitIfCurrent(e.terminalId,s.incarnation,e.timeoutMs)}catch(e){if(!this.isOwnedTerminal(s))return;throw e}if(!this.isOwnedTerminal(s))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.wait_result",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),exitCode:n.exitCode,output:n.output,state:n.state},s)}case"terminal.release":if(!this.manager.isCurrentSession(e.terminalId,s.incarnation))throw new TerminalNotFoundError(e.terminalId);const t=n.outbound.prepareForgetTerminal(e.clientId,e.terminalId);if(!this.manager.detachSessionIfCurrent(e.terminalId,s.incarnation))throw new TerminalNotFoundError(e.terminalId);return this.clients.get(e.terminalId)===s&&this.clients.delete(e.terminalId),this.dropPendingOutput(e.terminalId),void t();case"terminal.kill":if(!this.manager.killSessionIfCurrent(e.terminalId,s.incarnation,normalizeSignal(e.signal)))throw new TerminalNotFoundError(e.terminalId);return}}async execute(e,n){const t=this.outboundGeneration,i=t?.outbound;if(i?.isAvailable&&!i.isAvailable())throw new Error("terminal Service transport is unavailable; legacy fallback is forbidden");const r=e,s=r.context.terminal.terminalId,a=r.context.terminal.clientId;switch(r.name){case"terminal.open":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:e.params.columns,rows:e.params.rows,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command??t.shell,shell:t.shell,cwd:t.cwd,columns:t.cols,rows:t.rows,state:t.state}}case"terminal.input":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.writeToSessionIfCurrent(s,n.incarnation,Buffer.from(e.params.data,"base64"===e.params.encoding?"base64":"utf8")),{accepted:!0}}case"terminal.resize":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.resizeSessionIfCurrent(s,n.incarnation,e.params.columns,e.params.rows),{resized:!0}}case"terminal.close":{const e=this.assertAcpOwner(a,s);if(!this.manager.closeSessionIfCurrent(s,e.incarnation,r.params.reason))throw new TerminalNotFoundError(s);return{closed:!0}}case"terminal.ping":{const e=this.assertAcpOwner(a,s);return this.manager.touchSessionIfCurrent(s,e.incarnation),{terminalId:s,status:"ok"}}case"terminal.create":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:80,rows:24,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command}}case"terminal.wait":{const e=r,n=this.assertAcpOwner(a,s),t=await this.manager.waitForExitIfCurrent(s,n.incarnation,e.params.timeoutMs);if(!this.isOwnedAcpTerminal(n))throw new TerminalNotFoundError(s);return t}case"terminal.release":{const e=this.assertAcpOwner(a,s);if(!this.manager.detachSessionIfCurrent(s,e.incarnation))throw new TerminalNotFoundError(s);return this.clients.get(s)===e&&this.clients.delete(s),{released:!0}}case"terminal.kill":{const e=this.assertAcpOwner(a,s);if(!this.manager.killSessionIfCurrent(s,e.incarnation,normalizeSignal(r.params.signal)))throw new TerminalNotFoundError(s);return{killed:!0}}}throw new Error(`unsupported terminal command ${r.name}`)}nextSequence(e){return String(this.manager.nextServerSequence(e.incarnation.terminalId,e.incarnation))}sendOrClose(e,n){this.isOwnedTerminal(n)&&!n.generation.outbound.send(e)&&"terminal.output"!==e.type&&this.isOwnedTerminal(n)&&this.manager.closeSessionIfCurrent(e.terminalId,n.incarnation,"backpressure_limit")}emitFrame(e,n){if(!this.isOwnedTerminal(n))return;const t=this.openingFrames.get(e.terminalId);t?.owner===n?t.frames.push(e):this.sendOrClose(e,n)}assertOwner(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))throw new TerminalNotFoundError(n);return i}rebindServiceOwnerForResume(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.generation!==t||!this.isOwnedTerminal(i))return;if(i.clientId===e)return;if(this.flushPendingOutput(n),!this.manager.rebindSessionId(n,i.incarnation,e))throw new TerminalNotFoundError(n);t.outbound.rebindResumeClient(i.clientId,e,n),this.clientSequences.delete(i.clientId);const r={kind:"service",clientId:e,generation:i.generation,incarnation:i.incarnation};this.clients.set(n,r)}assertAcpOwner(e,n){const t=this.clients.get(n);if("acp"!==t?.kind||t.clientId!==e||!this.isOwnedAcpTerminal(t))throw new TerminalNotFoundError(n);return t}isGenerationActive(e){return this.outboundGeneration===e}isOwnedTerminal(e){return this.outboundGeneration===e.generation&&this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}isOwnedAcpTerminal(e){return this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}expireServiceTerminalReplay(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))return;const r=t.outbound.prepareForgetTerminal(e,n);if(!("closed"===this.manager.getSession(n)?.state?this.manager.releaseSessionIfCurrent(n,i.incarnation):this.manager.detachSessionIfCurrent(n,i.incarnation)))throw new TerminalNotFoundError(n);this.clients.get(n)===i&&this.clients.delete(n),this.openingFrames.get(n)?.owner===i&&this.openingFrames.delete(n),this.dropPendingOutput(n),r()}clearGenerationOwnership(e){for(const[n,t]of this.clients)"service"===t.kind&&t.generation===e&&this.clients.delete(n);for(const[n,t]of this.clientSequences)t.generation===e&&this.clientSequences.delete(n);for(const[n,t]of this.openingFrames)t.owner.generation===e&&this.openingFrames.delete(n);for(const[n,t]of this.pendingOutput)t.owner.generation===e&&this.dropPendingOutput(n)}queueOutput(e,n,t,i,r){let s=0,a=i;for(;s<t.length;){let i=this.pendingOutput.get(e);!i||i.owner===r&&i.stream===n||(this.flushPendingOutput(e),i=void 0),i||(i={owner:r,stream:n,chunks:[],bytes:0,sequence:a},this.pendingOutput.set(e,i));const o=Math.min(16384-i.bytes,t.length-s);i.chunks.push(t.subarray(s,s+o)),i.bytes+=o,i.sequence=a,s+=o,16384!==i.bytes?i.timer||(i.timer=setTimeout(()=>{this.pendingOutput.get(e)===i&&this.flushPendingOutput(e)},16),i.timer.unref?.()):(this.flushPendingOutput(e),s<t.length&&(a=this.manager.nextServerSequence(e,r.incarnation)))}}flushPendingOutput(e){const n=this.pendingOutput.get(e);if(!n)return;if(this.pendingOutput.delete(e),n.timer&&clearTimeout(n.timer),0===n.bytes||!this.isOwnedTerminal(n.owner))return;const t=1===n.chunks.length?n.chunks[0]:Buffer.concat(n.chunks,n.bytes);this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:n.owner.clientId,terminalId:e,sequence:String(n.sequence),stream:n.stream,data:t.toString("base64"),encoding:"base64"},n.owner)}dropPendingOutput(e){const n=this.pendingOutput.get(e);n&&(n.timer&&clearTimeout(n.timer),this.pendingOutput.delete(e))}}const runtimeServices=new WeakMap;export function getOrCreateRuntimeTerminalService(e){let n=runtimeServices.get(e);return n||(n=new RuntimeTerminalService(e),runtimeServices.set(e,n)),n}export function assertTerminalClientFrame(e){const n=e;if(!(n&&CLIENT_FRAME_TYPES.has(String(n.type))&&n.protocolVersion===TERMINAL_SERVICE_PROTOCOL_VERSION&&isUuid(n.clientId)&&isUuid(n.terminalId)&&isPositiveRuntimeSequence(n.clientSequence)))throw new Error("invalid terminal client frame");switch(e.type){case"terminal.open":assertOptionalCommand(e.command),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalDimension(e.columns),assertOptionalDimension(e.rows),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.create":("string"!=typeof e.command||e.command.length<1||e.command.length>4096)&&invalidFrame(),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.resume":isRuntimeSequence(e.lastServerSequence)||invalidFrame();break;case"terminal.input":("string"!=typeof e.data||"utf8"!==e.encoding&&"base64"!==e.encoding)&&invalidFrame();break;case"terminal.resize":assertDimension(e.columns),assertDimension(e.rows);break;case"terminal.close":assertOptionalString(e.reason);break;case"terminal.ping":"string"!=typeof e.nonce&&invalidFrame();break;case"terminal.wait":void 0!==e.timeoutMs&&(!Number.isInteger(e.timeoutMs)||e.timeoutMs<0||e.timeoutMs>864e5)&&invalidFrame();break;case"terminal.kill":assertOptionalString(e.signal)}}const CLIENT_FRAME_TYPES=new Set(["terminal.open","terminal.resume","terminal.input","terminal.resize","terminal.close","terminal.ping","terminal.create","terminal.wait","terminal.release","terminal.kill"]);function normalizeSignal(e){return e&&e.startsWith("SIG")?e:"SIGTERM"}const UUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,UINT64_MAX=18446744073709551615n;function isUuid(e){return"string"==typeof e&&UUID_PATTERN.test(e)}function isRuntimeSequence(e){return"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)<=UINT64_MAX}function isPositiveRuntimeSequence(e){return isRuntimeSequence(e)&&"0"!==e}function invalidFrame(){throw new Error("invalid terminal client frame")}function assertOptionalString(e){void 0!==e&&"string"!=typeof e&&invalidFrame()}function assertOptionalCommand(e){void 0!==e&&("string"!=typeof e||e.length<1||e.length>4096)&&invalidFrame()}function assertOptionalArgs(e){void 0===e||Array.isArray(e)&&e.every(e=>"string"==typeof e)||invalidFrame()}function assertOptionalEnv(e){void 0===e||e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>"string"==typeof e)||invalidFrame()}function assertDimension(e){(!Number.isInteger(e)||e<1||e>1e3)&&invalidFrame()}function assertOptionalDimension(e){void 0!==e&&assertDimension(e)}function assertOptionalOutputLimit(e){void 0!==e&&(!Number.isInteger(e)||e<1||e>16777216)&&invalidFrame()}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID}from"node:crypto";import{assertTerminalClientFrame}from"./runtime-terminal-service.js";import{systemTerminalAuthorityClock,verifyTerminalAuthority}from"./terminal-authority-contract.js";import{InMemoryTerminalAuthorityJournal}from"./terminal-authority-journal.js";export class TerminalAuthorityAdmission{journal;clock;constructor(r=new InMemoryTerminalAuthorityJournal,e=systemTerminalAuthorityClock){this.journal=r,this.clock=e}async admit(r){if(!isRecord(r))throw new Error("invalid terminal client frame");const e=
|
|
1
|
+
import{randomUUID}from"node:crypto";import{assertTerminalClientFrame}from"./runtime-terminal-service.js";import{systemTerminalAuthorityClock,verifyTerminalAuthority}from"./terminal-authority-contract.js";import{InMemoryTerminalAuthorityJournal}from"./terminal-authority-journal.js";export class TerminalAuthorityAdmission{journal;clock;constructor(r=new InMemoryTerminalAuthorityJournal,e=systemTerminalAuthorityClock){this.journal=r,this.clock=e}async admit(r){if(!isRecord(r))throw new Error("invalid terminal client frame");const e=verifyTerminalAuthority(r.terminalAuthority,this.clock);if(r.clientId!==e.clientId)throw new Error("terminal authority client mismatch");if("terminal.open"===r.type||"terminal.create"===r.type){if(e.resumeTerminalId)throw new Error("terminal resume requires terminal.resume");if(""!==r.terminalId)throw new Error("new terminal must be runtime allocated");assertTerminalClientFrame({...r,terminalId:VALIDATION_TERMINAL_ID});const t=await this.journal.bindNew(e,randomUUID());return{frame:{...r,terminalId:t.binding.terminalId},admission:{recovered:t.recovered}}}assertTerminalClientFrame(r);const t=r.terminalId;if("terminal.resume"===r.type){if(!e.resumeTerminalId||e.resumeTerminalId!==t)throw new Error("terminal resume id mismatch");return await this.journal.resume(e),{frame:r,admission:{recovered:!0}}}return await this.journal.assertBound(t,e),{frame:r,admission:{recovered:!1}}}}const VALIDATION_TERMINAL_ID="018f0000-0000-7000-8000-0000000000f0";function isRecord(r){return null!==r&&"object"==typeof r&&!Array.isArray(r)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";export const TERMINAL_AUTHORITY_SCHEMA_VERSION=1;export const TERMINAL_AUTHORITY_CONTRACT_REVISION="beeos.terminal-authority.v1";export const TERMINAL_AUTHORITY_SCHEMA_HASH="e1e1acbf9d3c7a8ee2377e7224d327b1a136069de7024c71e229702e997cdcea";export const TERMINAL_AUTHORITY_CONTRACT_DIGEST="88eaec1ec16ab48462c81b58a6c65920ba721e9f7adf074f6467373075950c8f";export const TERMINAL_AUTHORITY_CANONICALIZATION_REVISION="rfc8785-jcs";export const TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE="JCS(authority with ticketProof and authorityHash omitted)";export const TERMINAL_AUTHORITY_CLOCK_SKEW_MS=5e3;export const TERMINAL_AUTHORITY_MAX_LIFETIME_MS=6e4;const HEX_256=/^[0-9a-f]{64}$/,RFC3339_NANO=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/,NANOS_PER_MILLISECOND=1000000n,REQUIRED_KEYS=["schemaVersion","contractRevision","schemaHash","userId","instanceId","platformAgentId","clientId","ticketId","ticketProof","ticketProofHash","issuedAt","expiresAt","scopeHash","authorityHash"],AUTHORITY_KEYS=new Set([...REQUIRED_KEYS,"conversationId","resumeTerminalId"]);export const systemTerminalAuthorityClock=Object.freeze({now:()=>new Date});export const terminalAuthorityJcsPort=Object.freeze({revision:"rfc8785-jcs",canonicalize:t=>canonicalizeJcs(t),hash:t=>sha256(canonicalizeJcs(t))});export function verifyTerminalAuthorityGolden(t){if("beeos.terminal-authority.v1.golden"!==t.format||"rfc8785-jcs"!==t.canonicalization||"sha256"!==t.hashAlgorithm||t.contractDigest!==TERMINAL_AUTHORITY_CONTRACT_DIGEST||1!==t.contract.schemaVersion||"beeos.terminal-authority.v1"!==t.contract.contractRevision||t.contract.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH||t.contract.authorityHashPreimageRule!==TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE||sha256(t.contract.schemaCanonicalJson)!==TERMINAL_AUTHORITY_SCHEMA_HASH||terminalAuthorityJcsPort.hash(t.contract)!==t.contractDigest||0===t.fixtures.length)throw new Error("terminal authority JCS golden contract mismatch");for(const e of t.fixtures){const t=authorityHashPreimage(e.authority);if(terminalAuthorityJcsPort.canonicalize(JSON.parse(e.authorityHashPreimageJson))!==t||t!==e.authorityCanonicalJson||sha256(t)!==e.authorityHash||e.authorityHash!==e.authority.authorityHash)throw new Error(`terminal authority JCS golden fixture mismatch: ${e.name}`)}}export function verifyTerminalAuthority(t,e=systemTerminalAuthorityClock
|
|
1
|
+
import{createHash}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";export const TERMINAL_AUTHORITY_SCHEMA_VERSION=1;export const TERMINAL_AUTHORITY_CONTRACT_REVISION="beeos.terminal-authority.v1";export const TERMINAL_AUTHORITY_SCHEMA_HASH="e1e1acbf9d3c7a8ee2377e7224d327b1a136069de7024c71e229702e997cdcea";export const TERMINAL_AUTHORITY_CONTRACT_DIGEST="88eaec1ec16ab48462c81b58a6c65920ba721e9f7adf074f6467373075950c8f";export const TERMINAL_AUTHORITY_CANONICALIZATION_REVISION="rfc8785-jcs";export const TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE="JCS(authority with ticketProof and authorityHash omitted)";export const TERMINAL_AUTHORITY_CLOCK_SKEW_MS=5e3;export const TERMINAL_AUTHORITY_MAX_LIFETIME_MS=6e4;const HEX_256=/^[0-9a-f]{64}$/,RFC3339_NANO=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/,NANOS_PER_MILLISECOND=1000000n,REQUIRED_KEYS=["schemaVersion","contractRevision","schemaHash","userId","instanceId","platformAgentId","clientId","ticketId","ticketProof","ticketProofHash","issuedAt","expiresAt","scopeHash","authorityHash"],AUTHORITY_KEYS=new Set([...REQUIRED_KEYS,"conversationId","resumeTerminalId"]);export const systemTerminalAuthorityClock=Object.freeze({now:()=>new Date});export const terminalAuthorityJcsPort=Object.freeze({revision:"rfc8785-jcs",canonicalize:t=>canonicalizeJcs(t),hash:t=>sha256(canonicalizeJcs(t))});export function verifyTerminalAuthorityGolden(t){if("beeos.terminal-authority.v1.golden"!==t.format||"rfc8785-jcs"!==t.canonicalization||"sha256"!==t.hashAlgorithm||t.contractDigest!==TERMINAL_AUTHORITY_CONTRACT_DIGEST||1!==t.contract.schemaVersion||"beeos.terminal-authority.v1"!==t.contract.contractRevision||t.contract.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH||t.contract.authorityHashPreimageRule!==TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE||sha256(t.contract.schemaCanonicalJson)!==TERMINAL_AUTHORITY_SCHEMA_HASH||terminalAuthorityJcsPort.hash(t.contract)!==t.contractDigest||0===t.fixtures.length)throw new Error("terminal authority JCS golden contract mismatch");for(const e of t.fixtures){const t=authorityHashPreimage(e.authority);if(terminalAuthorityJcsPort.canonicalize(JSON.parse(e.authorityHashPreimageJson))!==t||t!==e.authorityCanonicalJson||sha256(t)!==e.authorityHash||e.authorityHash!==e.authority.authorityHash)throw new Error(`terminal authority JCS golden fixture mismatch: ${e.name}`)}}export function verifyTerminalAuthority(t,e=systemTerminalAuthorityClock){if(!isPlainObject(t)||Object.keys(t).some(t=>!AUTHORITY_KEYS.has(t))||REQUIRED_KEYS.some(e=>!(e in t)))throw new Error("terminal authority schema mismatch");const r=t;if(1!==r.schemaVersion||"beeos.terminal-authority.v1"!==r.contractRevision||r.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH)throw new Error("terminal authority contract mismatch");const i=requireString(r.userId,256,"userId"),o=requireString(r.instanceId,128,"instanceId"),n=requireString(r.platformAgentId,128,"platformAgentId"),a=optionalString(r.conversationId,128,"conversationId"),s=requireString(r.clientId,128,"clientId"),c=requireString(r.ticketId,128,"ticketId"),h=requireString(r.ticketProof,8192,"ticketProof"),u=requireDigest(r.ticketProofHash,"ticketProofHash"),m=requireTimestamp(r.issuedAt,"issuedAt"),I=requireTimestamp(r.expiresAt,"expiresAt"),l=optionalString(r.resumeTerminalId,128,"resumeTerminalId"),d=requireDigest(r.scopeHash,"scopeHash"),A=requireDigest(r.authorityHash,"authorityHash"),f=timestampNanos(m),T=timestampNanos(I),H=e.now().getTime();if(!Number.isFinite(H))throw new Error("terminal authority clock is invalid");const E=1000000n*BigInt(Math.trunc(H));if(T<=f)throw new Error("terminal authority time range is invalid");if(T-f>1000000n*BigInt(6e4))throw new Error("terminal authority lifetime exceeds maximum");if(f>E+1000000n*BigInt(5e3))throw new Error("terminal authority issue time is in the future");if(T<=E)throw new Error("terminal authority has expired");if(sha256(h)!==u)throw new Error("terminal authority ticket proof mismatch");const y=authorityHashPreimage(r);if(sha256(y)!==A)throw new Error("terminal authority integrity mismatch");return Object.freeze({schemaVersion:1,contractRevision:"beeos.terminal-authority.v1",schemaHash:TERMINAL_AUTHORITY_SCHEMA_HASH,userId:i,instanceId:o,platformAgentId:n,conversationId:a,clientId:s,ticketId:c,issuedAt:m,expiresAt:I,resumeTerminalId:l,scopeHash:d,authorityHash:A,terminalBindingHash:sha256(["terminal-binding-v1",i,o,n,a,s,c].join("\0"))})}export function authorityHashPreimage(t){const e=t,r={schemaVersion:e.schemaVersion,contractRevision:e.contractRevision,schemaHash:e.schemaHash,userId:e.userId,instanceId:e.instanceId,platformAgentId:e.platformAgentId,...e.conversationId?{conversationId:e.conversationId}:{},clientId:e.clientId,ticketId:e.ticketId,ticketProofHash:e.ticketProofHash,issuedAt:e.issuedAt,expiresAt:e.expiresAt,...e.resumeTerminalId?{resumeTerminalId:e.resumeTerminalId}:{},scopeHash:e.scopeHash};return terminalAuthorityJcsPort.canonicalize(r)}export const sha256=t=>createHash("sha256").update(t,"utf8").digest("hex");function requireString(t,e,r){if("string"!=typeof t||t.length<1||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function optionalString(t,e,r){if(void 0===t)return"";if("string"!=typeof t||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function requireDigest(t,e){if("string"!=typeof t||!HEX_256.test(t))throw new Error(`terminal authority ${e} is invalid`);return t}function requireTimestamp(t,e){if("string"!=typeof t||!RFC3339_NANO.test(t)||!Number.isFinite(Date.parse(t)))throw new Error(`terminal authority ${e} is invalid`);return t}function timestampNanos(t){const e=RFC3339_NANO.exec(t);if(!e)throw new Error("terminal authority timestamp is invalid");const r=Date.parse(`${e[1]}${e[3]}`),i=BigInt((e[2]??"").padEnd(9,"0"));return 1000000n*BigInt(r)+i}function isPlainObject(t){return null!==t&&"object"==typeof t&&!Array.isArray(t)&&(Object.getPrototypeOf(t)===Object.prototype||null===Object.getPrototypeOf(t))}function hasLoneSurrogate(t){for(let e=0;e<t.length;e++){const r=t.charCodeAt(e);if(r>=55296&&r<=56319){if(++e>=t.length||t.charCodeAt(e)<56320||t.charCodeAt(e)>57343)return!0}else if(r>=56320&&r<=57343)return!0}return!1}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{EventEmitter}from"node:events";import WebSocket from"ws";import{
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{EventEmitter}from"node:events";import WebSocket from"ws";import{signAgentMessage}from"../agent-auth.js";export function terminalAgentAuthSigningMessage(e){return`terminal|${e.public_key}|${e.timestamp}|${e.nonce}|${e.runtimeLeaseCredential}|${e.handlerIdentity}|${e.runtimeEpoch}|${e.leaseId}`}export function createTerminalLeaseBoundAgentAuth(e){if(!(32===e.keys.publicKey.length&&[32,48].includes(e.keys.privateKey.length)&&e.lease.instanceId&&e.lease.leaseId&&e.lease.handlerIdentity&&e.lease.runtimeLeaseCredential&&isPositiveRuntimeEpoch(e.lease.runtimeEpoch)&&Number.isSafeInteger(e.timestamp)&&!(e.timestamp<0)&&e.nonce))throw new Error("invalid Terminal runtime lease authority binding");const t={service:"terminal",public_key:Buffer.from(e.keys.publicKey).toString("base64"),timestamp:e.timestamp,nonce:e.nonce,runtimeLeaseCredential:e.lease.runtimeLeaseCredential,handlerIdentity:e.lease.handlerIdentity,runtimeEpoch:e.lease.runtimeEpoch,leaseId:e.lease.leaseId};return{type:"agent_auth",instance_id:e.lease.instanceId,...t,signature:Buffer.from(signAgentMessage(terminalAgentAuthSigningMessage(t),e.keys.privateKey)).toString("base64")}}export class TerminalLeaseBoundBridgeClient extends EventEmitter{options;state="idle";socket;unsubscribe;expiryTimer;reconnectTimer;generation=0;stopped=!0;authenticatedBindingHash;authorityEpochBindingHash;transition=Promise.resolve();constructor(e){super(),this.options=e}async connect(){this.unsubscribe||(this.unsubscribe=this.options.authority.subscribeAuthority(e=>this.onAuthorityChanged(e))),this.stopped=!1;const e=this.options.authority.authorityLease;if(!e)throw new Error("active runtime registration lease required for Terminal auth");await this.queueTransition(()=>this.replaceConnection(e))}async send(e){const t=this.socket;if("connected"!==this.state||!t||t.readyState!==WebSocket.OPEN)throw new Error("Terminal lease-bound Bridge is not connected");await new Promise((i,n)=>t.send(e,e=>e?n(e):i()))}async close(e=!0){this.stopped=!0,this.clearTimers(),this.unsubscribe?.(),this.unsubscribe=void 0,this.authenticatedBindingHash=void 0,this.authorityEpochBindingHash=void 0,this.generation++,await this.closeSocket(e),this.state="closed"}onAuthorityChanged(e){if(this.stopped)return;if(!e)return void this.fenceAuthority();this.scheduleExpiryFence(e);const t=authorityEpochHash(e),i=authorityBindingHash(e);return this.authorityEpochBindingHash&&t!==this.authorityEpochBindingHash?(this.authorityEpochBindingHash=t,this.emit("transportEpochChanged"),void this.queueTransition(()=>this.replaceConnection(e)).catch(e=>this.emitError(e))):i!==this.authenticatedBindingHash?"connected"===this.state&&this.authenticatedBindingHash?(this.authenticatedBindingHash=i,void(this.authorityEpochBindingHash??=t)):void this.queueTransition(()=>this.replaceConnection(e)).catch(e=>this.emitError(e)):void 0}async replaceConnection(e){if(this.stopped||null===this.options.authority.authorityLease)return;this.clearReconnectTimer();const t=++this.generation;if(await this.closeSocket(!1),this.stopped||t!==this.generation)return;this.state="connecting";const i=new WebSocket(`${this.options.bridgeUrl.replace(/\/+$/,"")}/agent`);this.socket=i;const n=authorityBindingHash(e);await new Promise((s,o)=>{let r=!1;const a=setTimeout(()=>{c(new Error("Terminal auth timeout")),i.readyState!==WebSocket.CLOSED&&i.terminate()},this.options.authTimeoutMs??1e4),h=()=>{clearTimeout(a),i.off("open",u)},c=e=>{h(),r||o(e)},u=()=>{const e=this.options.authority.authorityLease;if(!e||authorityBindingHash(e)!==n||t!==this.generation)return c(new Error("Terminal runtime lease changed during auth")),void i.close();const s=createTerminalLeaseBoundAgentAuth({lease:e,keys:this.options.keys,timestamp:Math.floor((this.options.now??Date.now)()/1e3),nonce:(this.options.nonce??randomUUID)()});i.send(JSON.stringify(s),e=>{e&&c(e)})};i.on("open",u),i.on("message",o=>{if(!r){try{if("auth_ok"!==JSON.parse(o.toString()).type)return}catch{return}return r=!0,h(),t!==this.generation?void i.close():(this.authenticatedBindingHash=n,this.authorityEpochBindingHash=authorityEpochHash(e),this.state="connected",this.scheduleExpiryFence(e),this.emit("connected"),void s())}this.emit("message",o)}),i.on("error",e=>{this.emitError(e),c(e)}),i.on("close",(e,i)=>{h(),r||o(new Error(`Terminal auth rejected: ${i.toString()||"connection closed"}`)),t===this.generation&&(this.socket=void 0,this.authenticatedBindingHash=void 0,this.state="idle",this.emit("disconnected",i.toString()||"connection closed"),1008!==e?!this.stopped&&this.options.authority.authorityLease&&this.scheduleReconnect():this.fenceAuthority())})})}fenceAuthority(){this.clearTimers(),this.authenticatedBindingHash=void 0,this.authorityEpochBindingHash=void 0,this.generation++;const e=this.socket;this.socket=void 0,this.state="idle",e&&e.readyState!==WebSocket.CLOSED&&e.terminate(),this.emit("authorityLost")}scheduleExpiryFence(e){this.expiryTimer&&clearTimeout(this.expiryTimer);const t=authorityBindingHash(e);this.expiryTimer=setTimeout(()=>{const e=this.options.authority.authorityLease;e?authorityBindingHash(e)===t&&(Date.parse(e.leaseExpiresAt)>(this.options.now??Date.now)()?this.scheduleExpiryFence(e):this.fenceAuthority()):this.fenceAuthority()},Math.min(2147483647,Math.max(0,Date.parse(e.leaseExpiresAt)-(this.options.now??Date.now)()))),this.expiryTimer.unref?.()}scheduleReconnect(){this.reconnectTimer||this.stopped||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0;const e=this.options.authority.authorityLease;e&&this.queueTransition(()=>this.replaceConnection(e)).catch(e=>{this.emitError(e),this.scheduleReconnect()})},this.options.reconnectDelayMs??1e3),this.reconnectTimer.unref?.())}async closeSocket(e){const t=this.socket;this.socket=void 0,t&&t.readyState!==WebSocket.CLOSED&&(e&&t.readyState===WebSocket.OPEN?await new Promise(e=>{t.once("close",()=>e()),t.close()}):t.terminate())}queueTransition(e){const t=this.transition.then(e,e);return this.transition=t.catch(()=>{}),t}clearReconnectTimer(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0}clearTimers(){this.clearReconnectTimer(),this.expiryTimer&&clearTimeout(this.expiryTimer),this.expiryTimer=void 0}emitError(e){this.listenerCount("error")>0&&this.emit("error",e instanceof Error?e:new Error(String(e)))}}function authorityEpochHash(e){return createHash("sha256").update(`${e.instanceId}\0${e.leaseId}\0${e.handlerIdentity}\0${e.runtimeEpoch}`).digest("hex")}function authorityBindingHash(e){return createHash("sha256").update(`${e.instanceId}\0${e.leaseId}\0${e.handlerIdentity}\0${e.runtimeEpoch}\0${e.runtimeLeaseCredential}\0${e.leaseExpiresAt}`).digest("hex")}function isPositiveRuntimeEpoch(e){return"string"==typeof e&&/^[1-9][0-9]*$/.test(e)&&BigInt(e)<=18446744073709551615n}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{access,constants}from"node:fs/promises";import{execFile}from"node:child_process";import{promisify}from"node:util";import{uploadLocalFile}from"./upload-file.js";const execFileAsync=promisify(execFile);export const BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME="beeos_desktop_screenshot";export const BEEOS_DESKTOP_SCREENSHOT_TOOL_DESCRIPTION="Capture the agent desktop (DISPLAY) via platform beeos-screenshot, then upload for the user. Prefer this over shell/python X11 dumps. Do not invent raw Xlib/PIL capture — those produce RGB striping. Requires openclaw-desktop image with scrot (or ImageMagick import) and /usr/local/bin/beeos-screenshot.";export const BEEOS_DESKTOP_SCREENSHOT_TOOL_SCHEMA={title:"BeeOS desktop screenshot",type:"object",description:BEEOS_DESKTOP_SCREENSHOT_TOOL_DESCRIPTION,additionalProperties:!1,properties:{path:{type:"string",minLength:1,description:"Local filesystem path for the PNG capture (default: /tmp/desktop.png)."},upload:{type:"boolean",description:"When true (default), upload the capture so the user sees a chat preview."}}};const DEFAULT_PATH="/tmp/desktop.png",REQUEST_TIMEOUT_MS=12e4,CAPTURE_TIMEOUT_MS=3e4,SCREENSHOT_BINARIES=["/usr/local/bin/beeos-screenshot","beeos-screenshot"],MISSING_BINARY_MSG="beeos-screenshot not found on PATH. Use the openclaw-desktop image with scrot installed, or install scrot + /usr/local/bin/beeos-screenshot. Do not fall back to raw Xlib/PIL capture.";let _deps={};export function setDesktopScreenshotDeps(e){_deps=e}export function resetDesktopScreenshotDeps(){_deps={}}export async function executeDesktopScreenshot(e,t,r){const
|
|
1
|
+
import{access,constants}from"node:fs/promises";import{execFile}from"node:child_process";import{promisify}from"node:util";import{uploadLocalFile}from"./upload-file.js";const execFileAsync=promisify(execFile);export const BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME="beeos_desktop_screenshot";export const BEEOS_DESKTOP_SCREENSHOT_TOOL_DESCRIPTION="Capture the agent desktop (DISPLAY) via platform beeos-screenshot, then upload for the user. Prefer this over shell/python X11 dumps. Do not invent raw Xlib/PIL capture — those produce RGB striping. Requires openclaw-desktop image with scrot (or ImageMagick import) and /usr/local/bin/beeos-screenshot.";export const BEEOS_DESKTOP_SCREENSHOT_TOOL_SCHEMA={title:"BeeOS desktop screenshot",type:"object",description:BEEOS_DESKTOP_SCREENSHOT_TOOL_DESCRIPTION,additionalProperties:!1,properties:{path:{type:"string",minLength:1,description:"Local filesystem path for the PNG capture (default: /tmp/desktop.png)."},upload:{type:"boolean",description:"When true (default), upload the capture so the user sees a chat preview."}}};const DEFAULT_PATH="/tmp/desktop.png",REQUEST_TIMEOUT_MS=12e4,CAPTURE_TIMEOUT_MS=3e4,SCREENSHOT_BINARIES=["/usr/local/bin/beeos-screenshot","beeos-screenshot"],MISSING_BINARY_MSG="beeos-screenshot not found on PATH. Use the openclaw-desktop image with scrot installed, or install scrot + /usr/local/bin/beeos-screenshot. Do not fall back to raw Xlib/PIL capture.";let _deps={};export function setDesktopScreenshotDeps(e){_deps=e}export function resetDesktopScreenshotDeps(){_deps={}}export async function executeDesktopScreenshot(e,t,r,o){const s=parseParams(e);if(!s.ok)return errorResult(s.error);const{path:n,upload:a}=s,i=_deps.execFile??execFileAsync,p=_deps.resolveBinary?await _deps.resolveBinary():await resolveScreenshotBinary();if(!p)return errorResult(MISSING_BINARY_MSG);try{await i(p,[n],{timeout:3e4,env:{...process.env,DISPLAY:process.env.DISPLAY||":1"},signal:o})}catch(e){if(o?.aborted)throw e;if("ENOENT"===e.code)return errorResult(MISSING_BINARY_MSG);return errorResult(`desktop capture failed: ${e instanceof Error?e.message:String(e)}`)}try{await access(n,constants.R_OK)}catch{return errorResult(`capture produced no readable file at ${n}`)}if(!a){const e={ok:!0,path:n};return{...e,content:[{type:"text",text:`Desktop captured to ${n} (not uploaded)`}],output:e,result:e,details:e,isError:!1}}if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — file upload requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const c=_deps.uploadLocalFile??uploadLocalFile;try{const e=await c(n,t.agentGatewayUrl,r,{signal:o,timeoutMs:12e4,fileNameOverride:`desktop-${Date.now()}.png`}),s=[{uri:e.uri,name:e.name,mimeType:e.mimeType,fileId:e.fileId}],a=`Desktop screenshot uploaded: ${s[0].name}`,i={ok:!0,path:n,files:s};return{...i,content:[{type:"text",text:a},{type:"resource_link",uri:s[0].uri,name:s[0].name,mimeType:s[0].mimeType}],output:i,result:i,details:i,isError:!1}}catch(e){if(o?.aborted)throw e;return errorResult(`capture ok at ${n} but upload failed: ${e instanceof Error?e.message:String(e)}`)}}async function resolveScreenshotBinary(){for(const e of SCREENSHOT_BINARIES){if(!e.includes("/"))return e;try{return await access(e,constants.X_OK),e}catch{continue}}return null}function parseParams(e){if(null==e)return{ok:!0,path:DEFAULT_PATH,upload:!0};if("object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e,r=new Set(["path","upload"]),o=Object.keys(t).filter(e=>!r.has(e));if(o.length>0)return{ok:!1,error:`unknown params: ${o.join(", ")}`};let s=DEFAULT_PATH;if(void 0!==t.path){if("string"!=typeof t.path||""===t.path.trim())return{ok:!1,error:"path must be a non-empty string"};s=t.path.trim()}let n=!0;if(void 0!==t.upload){if("boolean"!=typeof t.upload)return{ok:!1,error:"upload must be a boolean"};n=t.upload}return{ok:!0,path:s,upload:n}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{outstandingDeliverables,rememberSessionWorkspace,stashFinalizeAttachments}from"./delivery-ledger.js";import{getUploadDeps,uploadLocalFile}from"./upload-file.js";const MAX_FINALIZE_UPLOADS=5;export async function fileDeliveryFinalizeDecision(e){if(e.stopHookActive||!e.sessionKey)return;e.cwd&&rememberSessionWorkspace(e.sessionKey,e.cwd);const s=getUploadDeps();if(!s)return;const i=outstandingDeliverables(e.sessionKey).slice(0,5),o=[];for(const e of i)try{const i=await uploadLocalFile(e,s.
|
|
1
|
+
import{outstandingDeliverables,rememberSessionWorkspace,stashFinalizeAttachments}from"./delivery-ledger.js";import{getUploadDeps,uploadLocalFile}from"./upload-file.js";const MAX_FINALIZE_UPLOADS=5;export async function fileDeliveryFinalizeDecision(e){if(e.stopHookActive||!e.sessionKey)return;e.cwd&&rememberSessionWorkspace(e.sessionKey,e.cwd);const s=getUploadDeps();if(!s)return;const i=outstandingDeliverables(e.sessionKey).slice(0,5),o=[];for(const e of i)try{const i=await uploadLocalFile(e,s.gatewayUrl,s.keys);o.push({uri:i.uri,name:i.name,...i.mimeType?{mimeType:i.mimeType}:{}})}catch(i){s.logger?.warn?.(`[file-delivery] finalize upload failed path=${e}: ${i instanceof Error?i.message:String(i)}`)}stashFinalizeAttachments(e.sessionKey,o)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{basename}from"node:path";import{BEEOS_UPLOAD_TOOL_NAME}from"./tool.js";import{BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME}from"./desktop-screenshot-tool.js";import{extractUploadResourceLinks}from"./extract-upload-uris.js";import{resolveUploadToolResultForEnrichment,writeEnrichmentCache}from"./result-cache.js";import{tryExtractLocalFiles}from"./local-file-extractors.js";import{getUploadDeps,uploadLocalFile}from"./upload-file.js";const UPLOAD_TOOL_NAMES=new Set([BEEOS_UPLOAD_TOOL_NAME,BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME]);export function isUploadArtifactToolName(e){if(!e)return!1;if(UPLOAD_TOOL_NAMES.has(e))return!0;const t=e.toLowerCase();return t.includes("beeos_upload")||t.includes("beeos upload file")||"beeos file upload"===t||t.includes("beeos_desktop_screenshot")||t.includes("desktop screenshot")}export function isRemoteOrPlatformMediaUri(e){const t=e.trim();return!!t&&(!!t.startsWith("beeos-file://")||(!(!t.startsWith("http://")&&!t.startsWith("https://"))||!!t.startsWith("data:")))}export function isLocalMediaPath(e){const t=e.trim();return!(!t||isRemoteOrPlatformMediaUri(t))&&(!!t.startsWith("file://")||!!t.startsWith("/"))}function filePathFromMediaUri(e){const t=e.trim();if(t.startsWith("file://"))try{return decodeURIComponent(new URL(t).pathname)}catch{return t.slice(7)}return t}function linkNameFromUri(e){if(e.startsWith("beeos-file://"))return e.slice(13)||"file";try{const t=e.includes("://")?new URL(e).pathname:e;return basename(t)||"file"}catch{return"file"}}function pushUnique(e,t,o){o.uri&&!t.has(o.uri)&&(t.add(o.uri),e.push(o))}export async function resolveToolArtifactLinks(e){const t=[],o=new Set,a=getUploadDeps(),i=a?.logger;let r=!1;if(isUploadArtifactToolName(e.toolName))try{const a=await resolveUploadToolResultForEnrichment(e.toolCallId,e.toolName===BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME?BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME:BEEOS_UPLOAD_TOOL_NAME);if(null!=a)for(const e of extractUploadResourceLinks(a))pushUnique(t,o,e)}catch(t){i?.warn?.(`[enrich] resolveUploadToolResultForEnrichment failed toolCallId=${e.toolCallId}: ${t instanceof Error?t.message:String(t)}`)}if(a){const l=new Set,s=tryExtractLocalFiles({toolCallId:e.toolCallId,toolName:e.toolName,rawResult:e.rawResult,rawText:e.rawText,params:e.params,cwd:e.cwd});for(const n of s)if(!l.has(n.filePath))try{i?.debug?.(`[enrich] Uploading local file ${n.filePath} for toolCallId=${e.toolCallId}`);const s=await uploadLocalFile(n.filePath,a.
|
|
1
|
+
import{basename}from"node:path";import{BEEOS_UPLOAD_TOOL_NAME}from"./tool.js";import{BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME}from"./desktop-screenshot-tool.js";import{extractUploadResourceLinks}from"./extract-upload-uris.js";import{resolveUploadToolResultForEnrichment,writeEnrichmentCache}from"./result-cache.js";import{tryExtractLocalFiles}from"./local-file-extractors.js";import{getUploadDeps,uploadLocalFile}from"./upload-file.js";const UPLOAD_TOOL_NAMES=new Set([BEEOS_UPLOAD_TOOL_NAME,BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME]);export function isUploadArtifactToolName(e){if(!e)return!1;if(UPLOAD_TOOL_NAMES.has(e))return!0;const t=e.toLowerCase();return t.includes("beeos_upload")||t.includes("beeos upload file")||"beeos file upload"===t||t.includes("beeos_desktop_screenshot")||t.includes("desktop screenshot")}export function isRemoteOrPlatformMediaUri(e){const t=e.trim();return!!t&&(!!t.startsWith("beeos-file://")||(!(!t.startsWith("http://")&&!t.startsWith("https://"))||!!t.startsWith("data:")))}export function isLocalMediaPath(e){const t=e.trim();return!(!t||isRemoteOrPlatformMediaUri(t))&&(!!t.startsWith("file://")||!!t.startsWith("/"))}function filePathFromMediaUri(e){const t=e.trim();if(t.startsWith("file://"))try{return decodeURIComponent(new URL(t).pathname)}catch{return t.slice(7)}return t}function linkNameFromUri(e){if(e.startsWith("beeos-file://"))return e.slice(13)||"file";try{const t=e.includes("://")?new URL(e).pathname:e;return basename(t)||"file"}catch{return"file"}}function pushUnique(e,t,o){o.uri&&!t.has(o.uri)&&(t.add(o.uri),e.push(o))}export async function resolveToolArtifactLinks(e){const t=[],o=new Set,a=getUploadDeps(),i=a?.logger;let r=!1;if(isUploadArtifactToolName(e.toolName))try{const a=await resolveUploadToolResultForEnrichment(e.toolCallId,e.toolName===BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME?BEEOS_DESKTOP_SCREENSHOT_TOOL_NAME:BEEOS_UPLOAD_TOOL_NAME);if(null!=a)for(const e of extractUploadResourceLinks(a))pushUnique(t,o,e)}catch(t){i?.warn?.(`[enrich] resolveUploadToolResultForEnrichment failed toolCallId=${e.toolCallId}: ${t instanceof Error?t.message:String(t)}`)}if(a){const l=new Set,s=tryExtractLocalFiles({toolCallId:e.toolCallId,toolName:e.toolName,rawResult:e.rawResult,rawText:e.rawText,params:e.params,cwd:e.cwd});for(const n of s)if(!l.has(n.filePath))try{i?.debug?.(`[enrich] Uploading local file ${n.filePath} for toolCallId=${e.toolCallId}`);const s=await uploadLocalFile(n.filePath,a.gatewayUrl,a.keys,{fileNameOverride:n.suggestedName});i?.info?.(`[enrich] Uploaded: fileId=${s.fileId} toolCallId=${e.toolCallId}`),l.add(n.filePath),pushUnique(t,o,{uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}}),r=!0}catch(e){i?.warn?.(`[enrich] Upload failed for ${n.filePath}: ${e instanceof Error?e.message:String(e)}, skipping`)}for(const s of e.payloadMediaUrls??[]){if(!isLocalMediaPath(s))continue;const n=filePathFromMediaUri(s);if(!l.has(n))try{i?.debug?.(`[enrich] Uploading payload media path ${n} for toolCallId=${e.toolCallId}`);const s=await uploadLocalFile(n,a.gatewayUrl,a.keys);i?.info?.(`[enrich] Uploaded payload media: fileId=${s.fileId} toolCallId=${e.toolCallId}`),l.add(n),pushUnique(t,o,{uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}}),r=!0}catch(e){i?.warn?.(`[enrich] Payload media upload failed for ${n}: ${e instanceof Error?e.message:String(e)}, skipping`)}}}for(const a of e.payloadMediaUrls??[])isRemoteOrPlatformMediaUri(a)&&pushUnique(t,o,{uri:a.trim(),name:linkNameFromUri(a.trim())});if(t.length>0&&(r||t.some(e=>e.uri.startsWith("beeos-file://")))){const o=t.map(e=>({uri:e.uri,name:e.name,...e.mimeType?{mimeType:e.mimeType}:{}}));writeEnrichmentCache(e.toolCallId,o).catch(t=>{i?.warn?.(`[enrich] Cache write failed for toolCallId=${e.toolCallId}: ${t instanceof Error?t.message:String(t)}`)})}return t}
|
package/dist/upload/tool.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{constants}from"node:fs";import{access,stat}from"node:fs/promises";import{uploadLocalFile
|
|
1
|
+
import{constants}from"node:fs";import{access,stat}from"node:fs/promises";import{uploadLocalFile}from"./upload-file.js";import{isBeeosFileUri}from"../files/beeos-file-uri.js";export const BEEOS_UPLOAD_TOOL_NAME="beeos_upload_file";export const BEEOS_UPLOAD_TOOL_DESCRIPTION="Upload 1-5 local files from this machine to the user via the BeeOS platform. Accepts only local filesystem paths; never pass beeos-file:// URIs because they are already BeeOS platform resources. If the call succeeds, the files are already delivered to the user — do not use any other tool to send them again. Upload modified deliverables again even if the same path was uploaded earlier; unchanged contents may reuse an existing platform file. Any file type is accepted: text, images, documents, PDFs, archives, binaries.";export const BEEOS_UPLOAD_TOOL_SCHEMA={title:"BeeOS file upload",type:"object",description:BEEOS_UPLOAD_TOOL_DESCRIPTION,additionalProperties:!1,required:["paths"],properties:{paths:{type:"array",minItems:1,maxItems:5,description:"Local filesystem paths (1-5). Absolute or workspace-relative. Do not pass beeos-file:// URIs.",items:{type:"string",minLength:1,description:"Single local filesystem path, not a beeos-file:// URI."}}}};const MAX_PATHS=5,REQUEST_TIMEOUT_MS=12e4,MAX_CONCURRENCY=3,BEEOS_FILE_URI_UPLOAD_ERROR="beeos-file:// is already a BeeOS platform file URI, not a local path. Do not upload it; display it directly.";async function runWithConcurrency(e,t){const r=new Array(e.length);let a=0;async function o(){for(;a<e.length;){const t=a++;r[t]=await e[t]()}}const s=[];for(let r=0;r<Math.min(t,e.length);r++)s.push(o());return await Promise.all(s),r}async function validateFiles(e){return Promise.all(e.map(async e=>{try{await access(e,constants.R_OK);return(await stat(e)).isFile()?{path:e,ok:!0}:{path:e,ok:!1,error:"not a regular file"}}catch(t){const r=t.code;let a;return a="ENOENT"===r?"file not found":"ENOTDIR"===r?"path component is not a directory":"EACCES"===r?"permission denied":t instanceof Error?t.message:String(t),{path:e,ok:!1,error:a}}}))}export async function executeUpload(e,t,r,a){const o=parseParams(e);if(!o.ok)return errorResult(o.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — file upload requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign upload requests");const s=t.agentGatewayUrl,i=o.paths,n=(await validateFiles(i)).find(e=>!e.ok);if(n)return errorResult(`${n.path}: ${n.error}`);const l=[],p=i.map(e=>async()=>{try{return await uploadSingleFile(e,s,r,a)}catch(t){if(a?.aborted)throw t;return l.push(`${e}: ${t instanceof Error?t.message:String(t)}`),null}}),c=(await runWithConcurrency(p,3)).filter(e=>null!==e);if(0===c.length)return errorResult(`All uploads failed: ${l.join("; ")}`);const u=c.map(e=>({uri:`beeos-file://${e.fileId}`,name:e.fileName,mimeType:e.mimeType,fileId:e.fileId})),f=c.length===i.length?`Uploaded ${c.length} file(s): ${u.map(e=>e.name).join(", ")}`:`Uploaded ${c.length}/${i.length} file(s): ${u.map(e=>e.name).join(", ")}. Failed: ${l.join("; ")}`,m={ok:!0,files:u};return{...m,content:[{type:"text",text:f},...u.map(e=>({type:"resource_link",uri:e.uri,name:e.name,mimeType:e.mimeType}))],output:m,result:m,details:m,isError:!1}}async function uploadSingleFile(e,t,r,a){const o=await uploadLocalFile(e,t,r,{signal:a,timeoutMs:12e4});return{fileId:o.fileId,downloadUrl:"",fileName:o.name,mimeType:o.mimeType,size:o.size}}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e;if(Object.keys(t).filter(e=>"paths"!==e).length>0)return{ok:!1,error:"only paths is allowed"};if(!Array.isArray(t.paths))return{ok:!1,error:"paths must be an array"};if(t.paths.length<1)return{ok:!1,error:"paths must contain at least 1 item"};if(t.paths.length>5)return{ok:!1,error:"paths must contain no more than 5 items"};const r=[];for(let e=0;e<t.paths.length;e++){const a=t.paths[e];if("string"!=typeof a||""===a.trim())return{ok:!1,error:`paths[${e}] must be a non-empty string`};const o=a.trim();if(isBeeosFileUri(o))return{ok:!1,error:BEEOS_FILE_URI_UPLOAD_ERROR};r.push(o)}return{ok:!0,paths:r}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
|