From 04567379c62063949fb97674a17081eec74388e8 Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Fri, 6 Nov 2026 00:36:30 +0800 Subject: [PATCH 1/7] fix(agent): simplify parent authority test assertions Retain the parent-authority lint fixes. The upstream native executor replacement already supplies the service boxing change and removes OAR. Generated-by: Codex --- .../owner/tests/session_parent_authority.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/agent/native/src/owner/tests/session_parent_authority.rs b/plugins/agent/native/src/owner/tests/session_parent_authority.rs index 4c553e4622a..c365539d56e 100644 --- a/plugins/agent/native/src/owner/tests/session_parent_authority.rs +++ b/plugins/agent/native/src/owner/tests/session_parent_authority.rs @@ -226,23 +226,23 @@ async fn settled_self_response(f: &Fixture, record: &InteractionRecord) -> Value } _ => None, }); - if let Some(operation) = operation { - if let Some(outcome) = facts.iter().find_map(|fact| match fact { + if let Some(operation) = operation + && let Some(outcome) = facts.iter().find_map(|fact| match fact { Fact::ToolSettled { operation_id, outcome, } if operation_id == operation => Some(outcome), _ => None, - }) { - let ToolOutcome::Succeeded { - model_projection: DurableToolProjection::Json { value }, - .. - } = outcome - else { - panic!("self response must return a typed decision: {outcome:?}"); - }; - return value.clone(); - } + }) + { + let ToolOutcome::Succeeded { + model_projection: DurableToolProjection::Json { value }, + .. + } = outcome + else { + panic!("self response must return a typed decision: {outcome:?}"); + }; + return value.clone(); } commits.changed().await.unwrap(); } @@ -366,7 +366,7 @@ async fn model_parent_can_answer_only_direct_children_and_cannot_edit_permission let mut sibling_socket = start(&f, &listener, &sibling).await; let mut unrelated_socket = start(&f, &listener, &unrelated).await; let permissions = grant::Permissions { - filesystem: vec![Rule::exact(&f.workspace.join("result"), Access::Write)], + filesystem: vec![Rule::exact(f.workspace.join("result"), Access::Write)], network: Network::Denied, }; let answer = InteractionAnswer::Permissions { From 50d526466fc9f045792131b3df363b30bb2ed7e2 Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Wed, 4 Nov 2026 19:43:31 +0800 Subject: [PATCH 2/7] feat(agent): load checkpoints and restore local workspaces Load exact checkpoint revisions into isolated inspections with durable receipts and bounded history/artifact reads. Add workspace inventories, immutable file objects and explicit caller-attested or unverified capture. Expose Host publication and same-Root workspace restoration with explicit target permissions/model selection, original-operation recovery, directory ownership validation and Goal producer exclusion. Keep the Session ID and checkpoint Head for an explicit next turn and successor CAS publication. Refs #2370 Generated-by: Codex --- Cargo.lock | 2 + packages/plugin-sdk/src/contracts.ts | 20034 +++++++++------- packages/plugin-sdk/src/generated/methods.ts | 884 + platform/event-log/Cargo.toml | 2 + platform/event-log/README.md | 18 +- platform/event-log/README.zh-CN.md | 10 +- .../migrations/0018_checkpoint_loads.sql | 32 + platform/event-log/src/checkpoints.rs | 31 +- platform/event-log/src/checkpoints/load.rs | 88 + platform/event-log/src/checkpoints/restore.rs | 46 + .../event-log/src/checkpoints/workspace.rs | 59 + platform/event-log/src/root_store.rs | 3 +- .../event-log/src/root_store/checkpoints.rs | 127 +- .../src/root_store/checkpoints/journal.rs | 45 + .../src/root_store/checkpoints/load.rs | 476 + .../src/root_store/checkpoints/objects.rs | 111 +- .../src/root_store/checkpoints/restore.rs | 415 + .../src/root_store/checkpoints/tests.rs | 4 + .../src/root_store/checkpoints/tests/body.rs | 103 +- .../src/root_store/checkpoints/tests/load.rs | 272 + .../root_store/checkpoints/tests/workspace.rs | 504 + .../src/root_store/checkpoints/workspace.rs | 403 + platform/event-log/src/root_store/gates.rs | 1 + .../src/root_store/session_metadata.rs | 1 + platform/event-log/src/sessions/metadata.rs | 14 +- plugins/agent/api/README.md | 34 +- plugins/agent/api/README.zh-CN.md | 36 +- plugins/agent/api/src/contract.rs | 58 + plugins/agent/api/src/contract/checkpoints.rs | 257 +- plugins/agent/native/README.md | 6 +- plugins/agent/native/README.zh-CN.md | 6 +- plugins/agent/native/src/checkpoints.rs | 41 +- plugins/agent/native/src/owner/checkpoints.rs | 48 +- .../native/src/owner/checkpoints/load.rs | 218 + .../native/src/owner/checkpoints/restore.rs | 232 + .../native/src/owner/checkpoints/session.rs | 181 + .../native/src/owner/tests/checkpoints.rs | 4 + .../src/owner/tests/checkpoints/load.rs | 204 + .../src/owner/tests/checkpoints/workspace.rs | 210 + plugins/agent/native/src/service.rs | 113 +- .../maka/cli/src/acp/tests/public_client.rs | 13 + 41 files changed, 16974 insertions(+), 8372 deletions(-) create mode 100644 platform/event-log/migrations/0018_checkpoint_loads.sql create mode 100644 platform/event-log/src/checkpoints/load.rs create mode 100644 platform/event-log/src/checkpoints/restore.rs create mode 100644 platform/event-log/src/checkpoints/workspace.rs create mode 100644 platform/event-log/src/root_store/checkpoints/load.rs create mode 100644 platform/event-log/src/root_store/checkpoints/restore.rs create mode 100644 platform/event-log/src/root_store/checkpoints/tests/load.rs create mode 100644 platform/event-log/src/root_store/checkpoints/tests/workspace.rs create mode 100644 platform/event-log/src/root_store/checkpoints/workspace.rs create mode 100644 plugins/agent/native/src/owner/checkpoints/load.rs create mode 100644 plugins/agent/native/src/owner/checkpoints/restore.rs create mode 100644 plugins/agent/native/src/owner/checkpoints/session.rs create mode 100644 plugins/agent/native/src/owner/tests/checkpoints/load.rs create mode 100644 plugins/agent/native/src/owner/tests/checkpoints/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 08bc97cf35a..f9200198eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7874,6 +7874,8 @@ dependencies = [ name = "maka-event-log" version = "0.2.0" dependencies = [ + "cap-fs-ext", + "cap-std", "futures-util", "hex", "libc", diff --git a/packages/plugin-sdk/src/contracts.ts b/packages/plugin-sdk/src/contracts.ts index e5193644f88..7f997aaf0e5 100644 --- a/packages/plugin-sdk/src/contracts.ts +++ b/packages/plugin-sdk/src/contracts.ts @@ -7562,20 +7562,18 @@ export const contracts = { }, resources: {}, }, - historyCheckpointReceipt: { + historyCheckpointLoadReceipt: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointReceiptRead', + title: 'AgentCheckpointLoadReceiptRead', type: 'object', properties: { reference: { $ref: '#/$defs/RecoveryRef', }, input: { - description: - 'The exact original input. Changed input cannot select another publication.', - $ref: '#/$defs/AgentHistoryCheckpointPublish', + $ref: '#/$defs/AgentHistoryCheckpointRead', }, }, additionalProperties: false, @@ -7681,37 +7679,31 @@ export const contracts = { type: 'string', enum: ['journal_v1'], }, - AgentHistoryCheckpointPublish: { + AgentHistoryCheckpointRead: { type: 'object', properties: { sessionId: { type: 'string', minLength: 1, }, - expectedRevision: { - type: ['string', 'null'], - format: 'uuid', - }, - confirmedSubtreeDigest: { + revision: { description: - 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', + 'Only the exact currently retained revision is readable; no latest fallback.', type: 'string', - pattern: '^[a-f0-9]{64}$', + format: 'uuid', }, }, additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], + required: ['sessionId', 'revision'], }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointReceipt', + title: 'AgentCheckpointLoadReceipt', type: 'object', properties: { operation: { - description: - 'Original Host observation. Domain success never fabricates Host cleanup confirmation.', anyOf: [ { $ref: '#/$defs/OperationObservation', @@ -7721,12 +7713,10 @@ export const contracts = { }, ], }, - publication: { - description: - 'Authoritative domain result even when the Host response was lost.\nAbsence is not permission to recapture under a replacement operation.', + load: { anyOf: [ { - $ref: '#/$defs/AgentHistoryCheckpointPublication', + $ref: '#/$defs/AgentCheckpointLoad', }, { type: 'null', @@ -7935,83 +7925,107 @@ export const contracts = { }, ], }, - AgentHistoryCheckpointPublication: { + AgentCheckpointLoad: { type: 'object', properties: { + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, state: { - $ref: '#/$defs/AgentHistoryCheckpointState', + $ref: '#/$defs/AgentCheckpointLoadState', }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', + }, + additionalProperties: false, + required: ['head', 'inspection', 'state'], + }, + AgentHistoryCheckpointHead: { + type: 'object', + properties: { + revision: { + type: 'string', + format: 'uuid', + }, + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['state', 'cleanupPending'], + required: ['revision', 'manifest'], }, - AgentHistoryCheckpointState: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'capturing', - }, - }, - required: ['kind'], - additionalProperties: false, + AgentHistoryCheckpointObject: { + type: 'object', + properties: { + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'prepared', - }, - }, - required: ['kind'], - additionalProperties: false, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['digest', 'bytes'], + }, + AgentCheckpointInspection: { + type: 'object', + properties: { + sourceSessionId: { + type: 'string', + minLength: 1, + }, + operationId: { + type: 'string', + format: 'uuid', }, + }, + additionalProperties: false, + required: ['sourceSessionId', 'operationId'], + }, + AgentCheckpointLoadState: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'published', - }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', + const: 'preparing', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'head'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'conflict', + const: 'ready', }, - actual: { - type: ['string', 'null'], - format: 'uuid', + sessionIds: { + type: 'array', + items: { + type: 'string', + }, }, - }, - additionalProperties: false, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'interrupted', + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + { + type: 'null', + }, + ], }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'sessionIds'], }, { type: 'object', @@ -8026,1264 +8040,1009 @@ export const contracts = { }, ], }, - AgentHistoryCheckpointHead: { + AgentCheckpointWorkspaceSnapshot: { type: 'object', properties: { - revision: { + identity: { type: 'string', - format: 'uuid', }, - manifest: { + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + inventory: { $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['revision', 'manifest'], + required: ['identity', 'consistency', 'warning', 'inventory'], }, - AgentHistoryCheckpointObject: { - type: 'object', - properties: { - digest: { + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', type: 'string', - pattern: '^[a-f0-9]{64}$', + const: 'caller_confirmed_quiescent', }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', }, - }, - additionalProperties: false, - required: ['digest', 'bytes'], + ], + }, + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', + type: 'string', + const: 'external_writers_not_fenced', + }, + ], }, }, }, resources: {}, }, - historyLandmarks: { + historyCheckpointReceipt: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryLandmarksRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['sessionId', 'through'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryLandmarks', - description: - 'Sampled canonical display positions, not a complete Turn outline or an\nexecution grant.', + title: 'AgentHistoryCheckpointReceiptRead', type: 'object', properties: { - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + reference: { + $ref: '#/$defs/RecoveryRef', }, - landmarks: { - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryLandmark', - }, - maxItems: 64, + input: { + description: + 'The exact original input. Changed input cannot select another publication.', + $ref: '#/$defs/AgentHistoryCheckpointPublish', }, }, additionalProperties: false, - required: ['through', 'landmarks'], + required: ['reference', 'input'], $defs: { - AgentHistoryLandmark: { + RecoveryRef: { type: 'object', properties: { - turnId: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { type: 'string', - minLength: 1, }, - position: { - $ref: '#/$defs/HistoryRowPosition', + method: { + type: 'string', }, - lastPosition: { - $ref: '#/$defs/HistoryRowPosition', + operation: { + type: 'string', + format: 'uuid', }, - label: { + fingerprint: { type: 'string', - maxLength: 96, }, - origin: { - description: - "The Host's attribution of the Turn's opening message, when it generated it.", - anyOf: [ - { - $ref: '#/$defs/MessageOrigin', - }, - { - type: 'null', - }, - ], + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['turnId', 'position', 'lastPosition', 'label'], - }, - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', - type: 'object', - properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], + }, + DeploymentRef: { + type: 'object', + properties: { + stateDomain: { + type: 'string', }, - itemIndex: { + deployment: { + type: 'string', + format: 'uuid', + }, + revision: { type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['stateDomain', 'deployment', 'revision'], }, - MessageOrigin: { + InstanceId: { description: - 'Trusted producer attribution, preserved independently of editable content.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'session', - }, - invocation: { - $ref: '#/$defs/Invocation', - }, - }, - additionalProperties: false, - required: ['kind', 'invocation'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'goal', - }, - goalId: { - type: 'string', - }, - objective: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'goalId', 'objective'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'workhub_task', - }, - taskId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'taskId'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'workhub_graph', - }, - graphId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'graphId'], - }, - { - description: 'A worker conversation created by the Bot finished a run.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'bot_worker', - }, - sessionId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], - }, - { - description: - "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", - type: 'object', - properties: { - kind: { - type: 'string', - const: 'plan', - }, - executionId: { - type: 'string', - }, - title: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'executionId', 'title'], - }, - ], + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, }, - Invocation: { + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - session_id: { + name: { type: 'string', }, - turn_id: { - type: 'string', + version: { + type: 'integer', + format: 'uint32', + minimum: 0, }, - run_id: { - type: 'string', + transport: { + $ref: '#/$defs/Transport', }, - invocation_id: { + digest: { type: 'string', }, }, - required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], }, - }, - }, - resources: {}, - }, - historyPrompt: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryPromptRead', - type: 'object', - properties: { - sessionId: { + Transport: { type: 'string', - minLength: 1, - }, - through: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + enum: ['local', 'remote'], }, - position: { - $ref: '#/$defs/HistoryRowPosition', + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], }, - }, - additionalProperties: false, - required: ['sessionId', 'through', 'position'], - $defs: { - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + AgentHistoryCheckpointPublish: { type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + sessionId: { + type: 'string', + minLength: 1, }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + expectedRevision: { + type: ['string', 'null'], + format: 'uuid', + }, + confirmedSubtreeDigest: { + description: + 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', + type: 'string', + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentHistoryPrompt', - anyOf: [ - { - $ref: '#/$defs/AgentHistoryPrompt', + title: 'AgentHistoryCheckpointReceipt', + type: 'object', + properties: { + operation: { + description: + 'Original Host observation. Domain success never fabricates Host cleanup confirmation.', + anyOf: [ + { + $ref: '#/$defs/OperationObservation', + }, + { + type: 'null', + }, + ], }, - { - type: 'null', + publication: { + description: + 'Authoritative domain result even when the Host response was lost.\nAbsence is not permission to recapture under a replacement operation.', + anyOf: [ + { + $ref: '#/$defs/AgentHistoryCheckpointPublication', + }, + { + type: 'null', + }, + ], }, - ], + }, + additionalProperties: false, $defs: { - AgentHistoryPrompt: { - description: - 'A bounded presentation preview; the original message remains in history.', + OperationObservation: { type: 'object', properties: { - position: { - $ref: '#/$defs/HistoryRowPosition', + reference: { + $ref: '#/$defs/RecoveryRef', }, - text: { - type: 'string', - maxLength: 4096, + outcome: { + $ref: '#/$defs/OperationOutcome', + }, + cleanup: { + $ref: '#/$defs/OperationCleanup', }, }, additionalProperties: false, - required: ['position', 'text'], + required: ['reference', 'outcome', 'cleanup'], }, - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + RecoveryRef: { type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + source: { + $ref: '#/$defs/DeploymentRef', }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + namespace: { + $ref: '#/$defs/InstanceId', }, - }, - additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], - }, - }, - }, - resources: {}, - }, - import: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentImportChange', - oneOf: [ - { - type: 'object', - properties: { - kind: { + export: { type: 'string', - const: 'save_source', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + method: { + type: 'string', }, - id: { - type: ['string', 'null'], + operation: { + type: 'string', format: 'uuid', }, - name: { + fingerprint: { type: 'string', - minLength: 1, }, - location: { - $ref: '#/$defs/AgentImportLocation', + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['kind', 'revision', 'id', 'name', 'location'], + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - { + DeploymentRef: { type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'remove_source', + }, + deployment: { + type: 'string', + format: 'uuid', }, revision: { type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, - }, - id: { - type: 'string', - format: 'uuid', }, }, additionalProperties: false, - required: ['kind', 'revision', 'id'], + required: ['stateDomain', 'deployment', 'revision'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - kind: { + name: { type: 'string', - const: 'copy', }, - selection: { - $ref: '#/$defs/AgentImportSelection', + version: { + type: 'integer', + format: 'uint32', + minimum: 0, }, - path: { - type: 'string', - minLength: 1, + transport: { + $ref: '#/$defs/Transport', }, - title: { + digest: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'selection', 'path', 'title'], + required: ['name', 'version', 'transport', 'digest'], }, - ], - $defs: { - AgentImportLocation: { + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + OperationOutcome: { + description: + 'Unresolved means no durable result is known, including after process loss.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'codex', + const: 'unresolved', }, - path: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'returned', }, + value: true, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'value'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'claude_code', + const: 'unknown', }, - path: { + diagnostic: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'diagnostic'], }, + ], + }, + OperationCleanup: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'open_code', + const: 'unconfirmed', }, - path: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'confirmed', }, - database: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'failed', + }, + diagnostic: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path', 'database'], + required: ['kind', 'diagnostic'], }, ], }, - AgentImportSelection: { + AgentHistoryCheckpointPublication: { type: 'object', properties: { - source: { - $ref: '#/$defs/AgentImportSourceRef', - }, - entry: { - $ref: '#/$defs/AgentImportEntryRef', + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', }, - digest: { - type: 'string', - pattern: '^[a-f0-9]{64}$', + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', }, }, additionalProperties: false, - required: ['source', 'entry', 'digest'], + required: ['state', 'cleanupPending'], }, - AgentImportSourceRef: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', + AgentHistoryCheckpointState: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capturing', + }, + }, + required: ['kind'], + additionalProperties: false, }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'prepared', + }, + }, + required: ['kind'], + additionalProperties: false, }, - }, - additionalProperties: false, - required: ['id', 'revision'], - }, - AgentImportEntryRef: { - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - path: { - type: 'string', - minLength: 1, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'published', + }, + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + }, + additionalProperties: false, + required: ['kind', 'head'], }, - }, - additionalProperties: false, - required: ['sessionId', 'path'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentImportResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'sources', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'conflict', + }, + actual: { + type: ['string', 'null'], + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['kind'], }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'interrupted', + }, + }, + required: ['kind'], + additionalProperties: false, }, - sources: { - type: 'array', - items: { - $ref: '#/$defs/AgentImportSource', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, }, + required: ['kind'], + additionalProperties: false, }, - }, - additionalProperties: false, - required: ['kind', 'revision', 'sources'], + ], }, - { + AgentHistoryCheckpointHead: { type: 'object', properties: { - kind: { + revision: { type: 'string', - const: 'imported', + format: 'uuid', }, - session: { - $ref: '#/$defs/AgentSession', + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['kind', 'session'], + required: ['revision', 'manifest'], }, - { + AgentHistoryCheckpointObject: { type: 'object', properties: { - kind: { + digest: { type: 'string', - const: 'changed', + pattern: '^[a-f0-9]{64}$', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, - required: ['kind'], additionalProperties: false, + required: ['digest', 'bytes'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unavailable', - }, + }, + }, + resources: {}, + }, + historyLandmarks: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryLandmarksRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sessionId', 'through'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryLandmarks', + description: + 'Sampled canonical display positions, not a complete Turn outline or an\nexecution grant.', + type: 'object', + properties: { + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + landmarks: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryLandmark', }, - required: ['kind'], - additionalProperties: false, + maxItems: 64, }, - { + }, + additionalProperties: false, + required: ['through', 'landmarks'], + $defs: { + AgentHistoryLandmark: { type: 'object', properties: { - kind: { + turnId: { type: 'string', - const: 'invalid', + minLength: 1, + }, + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + lastPosition: { + $ref: '#/$defs/HistoryRowPosition', + }, + label: { + type: 'string', + maxLength: 96, + }, + origin: { + description: + "The Host's attribution of the Turn's opening message, when it generated it.", + anyOf: [ + { + $ref: '#/$defs/MessageOrigin', + }, + { + type: 'null', + }, + ], }, }, - required: ['kind'], additionalProperties: false, + required: ['turnId', 'position', 'lastPosition', 'label'], }, - ], - $defs: { - AgentImportSource: { + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - id: { - type: 'string', - format: 'uuid', - }, - name: { - type: 'string', - minLength: 1, - }, - location: { - $ref: '#/$defs/AgentImportLocation', + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - path: { - type: 'string', - minLength: 1, + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['id', 'name', 'location', 'path'], + required: ['sourceSequence', 'itemIndex'], }, - AgentImportLocation: { + MessageOrigin: { + description: + 'Trusted producer attribution, preserved independently of editable content.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'codex', + const: 'session', }, - path: { - type: 'string', - minLength: 1, + invocation: { + $ref: '#/$defs/Invocation', }, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'invocation'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'claude_code', + const: 'goal', }, - path: { + goalId: { + type: 'string', + }, + objective: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'goalId', 'objective'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'open_code', - }, - path: { - type: 'string', - minLength: 1, + const: 'workhub_task', }, - database: { + taskId: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path', 'database'], + required: ['kind', 'taskId'], }, - ], - }, - AgentSession: { - type: 'object', - properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { - type: 'string', - }, - title: { - type: 'string', - }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - archived: { - type: 'boolean', - }, - pinned: { - type: 'boolean', - }, - hasUnread: { - type: 'boolean', - }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], - }, - activity: { - $ref: '#/$defs/AgentSessionActivity', - }, - }, - additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], - }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ { - description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'start', + const: 'workhub_graph', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { + graphId: { type: 'string', - const: 'end', }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'graphId'], }, { - description: 'Include the complete target Turn.', + description: 'A worker conversation created by the Bot finished a run.', type: 'object', properties: { kind: { type: 'string', - const: 'through_turn', + const: 'bot_worker', }, - turnId: { + sessionId: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind', 'sessionId'], }, { description: - 'Exclude the complete target Turn, including its submitted inputs.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], - }, - ], - }, - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ - { + "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", type: 'object', properties: { kind: { type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, + const: 'plan', }, - modelId: { + executionId: { type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], }, - }, - additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], - }, - { - type: 'object', - properties: { - kind: { + title: { type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'selection'], + required: ['kind', 'executionId', 'title'], }, ], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - AgentExecutorSelection: { + Invocation: { type: 'object', properties: { - executor: { + session_id: { type: 'string', - minLength: 1, - }, - settings: { - $ref: '#/$defs/ExecutorSettings', }, - }, - additionalProperties: false, - required: ['executor', 'settings'], - }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', - type: 'object', - properties: { - model: { - type: ['string', 'null'], + turn_id: { + type: 'string', }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + run_id: { + type: 'string', }, - runtime: { - type: ['string', 'null'], + invocation_id: { + type: 'string', }, }, - additionalProperties: false, + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", + }, + }, + resources: {}, + }, + historyPrompt: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryPromptRead', + type: 'object', + properties: { + sessionId: { type: 'string', + minLength: 1, }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', - type: 'object', - properties: { - stateDomain: { - type: 'string', - }, - deployment: { - type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + through: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['access', 'requestPermissions'], + position: { + $ref: '#/$defs/HistoryRowPosition', }, - AgentWorkspaceAccess: { + }, + additionalProperties: false, + required: ['sessionId', 'through', 'position'], + $defs: { + HistoryRowPosition: { description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], - }, - BindingStamp: { + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - id: { - type: 'string', - format: 'uuid', + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - permissionEpoch: { + itemIndex: { type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['sourceSequence', 'itemIndex'], }, - AgentWorkspace: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentHistoryPrompt', + anyOf: [ + { + $ref: '#/$defs/AgentHistoryPrompt', + }, + { + type: 'null', + }, + ], + $defs: { + AgentHistoryPrompt: { + description: + 'A bounded presentation preview; the original message remains in history.', type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, - path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + position: { + $ref: '#/$defs/HistoryRowPosition', }, - identity: { + text: { type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + maxLength: 4096, }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: ['position', 'text'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - base_commit: { - type: 'string', + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - directory: { - type: 'string', + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], + required: ['sourceSequence', 'itemIndex'], }, }, }, resources: {}, }, - importDirectories: { - mode: 'query', + import: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectories', + title: 'AgentImportChange', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'roots', + const: 'save_source', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: - 'The directories inside `path`, a page at a time; `after` is the last\nname of the previous page.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'children', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - path: { + id: { + type: ['string', 'null'], + format: 'uuid', + }, + name: { type: 'string', minLength: 1, }, - after: { - type: ['string', 'null'], + location: { + $ref: '#/$defs/AgentImportLocation', }, }, additionalProperties: false, - required: ['kind', 'path', 'after'], + required: ['kind', 'revision', 'id', 'name', 'location'], }, - ], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectoryPage', - oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'roots', + const: 'remove_source', }, - roots: { - type: 'array', - items: { - $ref: '#/$defs/AgentDirectory', - }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + id: { + type: 'string', + format: 'uuid', }, }, additionalProperties: false, - required: ['kind', 'roots'], + required: ['kind', 'revision', 'id'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'children', - }, - directory: { - $ref: '#/$defs/AgentDirectory', + const: 'copy', }, - children: { - type: 'array', - items: { - $ref: '#/$defs/AgentDirectory', - }, - maxItems: 32, - }, - after: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['kind', 'directory', 'children', 'after'], - }, - ], - $defs: { - AgentDirectory: { - type: 'object', - properties: { - name: { - type: 'string', + selection: { + $ref: '#/$defs/AgentImportSelection', }, path: { type: 'string', + minLength: 1, }, - }, - additionalProperties: false, - required: ['name', 'path'], - }, - }, - }, - resources: {}, - }, - imports: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentImportsRead', - oneOf: [ - { - type: 'object', - properties: { - kind: { + title: { type: 'string', - const: 'sources', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'selection', 'path', 'title'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'catalog', + ], + $defs: { + AgentImportLocation: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'codex', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], }, - source: { - $ref: '#/$defs/AgentImportSourceRef', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'claude_code', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], }, - query: { - $ref: '#/$defs/AgentImportQuery', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'open_code', + }, + path: { + type: 'string', + minLength: 1, + }, + database: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path', 'database'], }, - }, - additionalProperties: false, - required: ['kind', 'source', 'query'], + ], }, - { + AgentImportSelection: { type: 'object', properties: { - kind: { - type: 'string', - const: 'preview', - }, source: { $ref: '#/$defs/AgentImportSourceRef', }, @@ -9291,34 +9050,13 @@ export const contracts = { $ref: '#/$defs/AgentImportEntryRef', }, digest: { - type: ['string', 'null'], - }, - after: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['kind', 'source', 'entry', 'digest', 'after'], - }, - { - type: 'object', - properties: { - kind: { type: 'string', - const: 'copies', - }, - after: { - type: ['string', 'null'], + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['kind', 'after'], + required: ['source', 'entry', 'digest'], }, - ], - $defs: { AgentImportSourceRef: { type: 'object', properties: { @@ -9336,22 +9074,6 @@ export const contracts = { additionalProperties: false, required: ['id', 'revision'], }, - AgentImportQuery: { - type: 'object', - properties: { - text: { - type: 'string', - }, - includeArchived: { - type: 'boolean', - }, - cursor: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['text', 'includeArchived', 'cursor'], - }, AgentImportEntryRef: { type: 'object', properties: { @@ -9371,7 +9093,7 @@ export const contracts = { }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentImportsPage', + title: 'AgentImportResult', oneOf: [ { type: 'object', @@ -9400,59 +9122,14 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'catalog', - }, - source: { - $ref: '#/$defs/AgentImportSourceRef', - }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentImportEntry', - }, - maxItems: 16, - }, - next: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['kind', 'source', 'entries', 'next'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'preview', - }, - preview: { - $ref: '#/$defs/AgentImportPreview', - }, - }, - additionalProperties: false, - required: ['kind', 'preview'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'copies', - }, - sessions: { - type: 'array', - items: { - $ref: '#/$defs/AgentSession', - }, - maxItems: 16, + const: 'imported', }, - next: { - type: ['string', 'null'], + session: { + $ref: '#/$defs/AgentSession', }, }, additionalProperties: false, - required: ['kind', 'sessions', 'next'], + required: ['kind', 'session'], }, { type: 'object', @@ -9564,194 +9241,42 @@ export const contracts = { }, ], }, - AgentImportSourceRef: { + AgentSession: { type: 'object', properties: { - id: { - type: 'string', - format: 'uuid', + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], }, - }, - additionalProperties: false, - required: ['id', 'revision'], - }, - AgentImportEntry: { - type: 'object', - properties: { - id: { - type: 'string', + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - path: { - type: 'string', + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, }, - title: { - type: 'string', - }, - cwd: { - type: ['string', 'null'], - }, - updatedAt: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - archived: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['id', 'path', 'title', 'cwd', 'updatedAt', 'archived'], - }, - AgentImportPreview: { - type: 'object', - properties: { - selection: { - $ref: '#/$defs/AgentImportSelection', - }, - title: { - type: 'string', - }, - cwd: { - type: ['string', 'null'], - }, - records: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - incompleteTail: { - type: 'boolean', - }, - items: { - type: 'array', - items: { - $ref: '#/$defs/AgentImportPreviewItem', - }, - maxItems: 8, - }, - next: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: [ - 'selection', - 'title', - 'cwd', - 'records', - 'bytes', - 'incompleteTail', - 'items', - 'next', - ], - }, - AgentImportSelection: { - type: 'object', - properties: { - source: { - $ref: '#/$defs/AgentImportSourceRef', - }, - entry: { - $ref: '#/$defs/AgentImportEntryRef', - }, - digest: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - }, - additionalProperties: false, - required: ['source', 'entry', 'digest'], - }, - AgentImportEntryRef: { - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId', 'path'], - }, - AgentImportPreviewItem: { - type: 'object', - properties: { - index: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - kind: { - $ref: '#/$defs/AgentImportRecordKind', - }, - text: { - type: 'string', - maxLength: 4096, - }, - truncated: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['index', 'kind', 'text', 'truncated'], - }, - AgentImportRecordKind: { - type: 'string', - enum: ['user', 'assistant', 'tool_call', 'tool_result', 'note'], - }, - AgentSession: { - type: 'object', - properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', + dataSource: { + $ref: '#/$defs/AgentDataSource', }, permissions: { $ref: '#/$defs/AgentWorkspacePermissions', @@ -10142,1502 +9667,4493 @@ export const contracts = { }, resources: {}, }, - interaction: { + importDirectories: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentInteractionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, + title: 'AgentDirectories', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'roots', + }, + }, + required: ['kind'], + additionalProperties: false, }, - requestId: { - type: 'string', - minLength: 1, + { + description: + 'The directories inside `path`, a page at a time; `after` is the last\nname of the previous page.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'children', + }, + path: { + type: 'string', + minLength: 1, + }, + after: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'path', 'after'], }, - }, - additionalProperties: false, - required: ['sessionId', 'requestId'], + ], }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'InteractionRecord', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - turnId: { - type: 'string', - }, - runId: { - type: 'string', - }, - requestId: { - type: 'string', - }, - createdAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - request: { - $ref: '#/$defs/InteractionRequest', - }, - outcome: { - anyOf: [ - { - $ref: '#/$defs/InteractionOutcome', + title: 'AgentDirectoryPage', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'roots', }, - { - type: 'null', + roots: { + type: 'array', + items: { + $ref: '#/$defs/AgentDirectory', + }, }, - ], + }, + additionalProperties: false, + required: ['kind', 'roots'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'children', + }, + directory: { + $ref: '#/$defs/AgentDirectory', + }, + children: { + type: 'array', + items: { + $ref: '#/$defs/AgentDirectory', + }, + maxItems: 32, + }, + after: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'directory', 'children', 'after'], + }, + ], + $defs: { + AgentDirectory: { + type: 'object', + properties: { + name: { + type: 'string', + }, + path: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['name', 'path'], + }, + }, + }, + resources: {}, + }, + imports: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentImportsRead', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'sources', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'catalog', + }, + source: { + $ref: '#/$defs/AgentImportSourceRef', + }, + query: { + $ref: '#/$defs/AgentImportQuery', + }, + }, + additionalProperties: false, + required: ['kind', 'source', 'query'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'preview', + }, + source: { + $ref: '#/$defs/AgentImportSourceRef', + }, + entry: { + $ref: '#/$defs/AgentImportEntryRef', + }, + digest: { + type: ['string', 'null'], + }, + after: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['kind', 'source', 'entry', 'digest', 'after'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'copies', + }, + after: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'after'], }, - }, - additionalProperties: false, - required: [ - 'sessionId', - 'turnId', - 'runId', - 'requestId', - 'createdAt', - 'request', - 'outcome', ], $defs: { - InteractionRequest: { + AgentImportSourceRef: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['id', 'revision'], + }, + AgentImportQuery: { + type: 'object', + properties: { + text: { + type: 'string', + }, + includeArchived: { + type: 'boolean', + }, + cursor: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['text', 'includeArchived', 'cursor'], + }, + AgentImportEntryRef: { + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId', 'path'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentImportsPage', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'sources', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + sources: { + type: 'array', + items: { + $ref: '#/$defs/AgentImportSource', + }, + }, + }, + additionalProperties: false, + required: ['kind', 'revision', 'sources'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'catalog', + }, + source: { + $ref: '#/$defs/AgentImportSourceRef', + }, + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentImportEntry', + }, + maxItems: 16, + }, + next: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'source', 'entries', 'next'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'preview', + }, + preview: { + $ref: '#/$defs/AgentImportPreview', + }, + }, + additionalProperties: false, + required: ['kind', 'preview'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'copies', + }, + sessions: { + type: 'array', + items: { + $ref: '#/$defs/AgentSession', + }, + maxItems: 16, + }, + next: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'sessions', 'next'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'changed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'invalid', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + ], + $defs: { + AgentImportSource: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + name: { + type: 'string', + minLength: 1, + }, + location: { + $ref: '#/$defs/AgentImportLocation', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['id', 'name', 'location', 'path'], + }, + AgentImportLocation: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'codex', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'claude_code', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'open_code', + }, + path: { + type: 'string', + minLength: 1, + }, + database: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path', 'database'], + }, + ], + }, + AgentImportSourceRef: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['id', 'revision'], + }, + AgentImportEntry: { + type: 'object', + properties: { + id: { + type: 'string', + }, + path: { + type: 'string', + }, + title: { + type: 'string', + }, + cwd: { + type: ['string', 'null'], + }, + updatedAt: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['id', 'path', 'title', 'cwd', 'updatedAt', 'archived'], + }, + AgentImportPreview: { + type: 'object', + properties: { + selection: { + $ref: '#/$defs/AgentImportSelection', + }, + title: { + type: 'string', + }, + cwd: { + type: ['string', 'null'], + }, + records: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + incompleteTail: { + type: 'boolean', + }, + items: { + type: 'array', + items: { + $ref: '#/$defs/AgentImportPreviewItem', + }, + maxItems: 8, + }, + next: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: [ + 'selection', + 'title', + 'cwd', + 'records', + 'bytes', + 'incompleteTail', + 'items', + 'next', + ], + }, + AgentImportSelection: { + type: 'object', + properties: { + source: { + $ref: '#/$defs/AgentImportSourceRef', + }, + entry: { + $ref: '#/$defs/AgentImportEntryRef', + }, + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', + }, + }, + additionalProperties: false, + required: ['source', 'entry', 'digest'], + }, + AgentImportEntryRef: { + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId', 'path'], + }, + AgentImportPreviewItem: { + type: 'object', + properties: { + index: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + kind: { + $ref: '#/$defs/AgentImportRecordKind', + }, + text: { + type: 'string', + maxLength: 4096, + }, + truncated: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['index', 'kind', 'text', 'truncated'], + }, + AgentImportRecordKind: { + type: 'string', + enum: ['user', 'assistant', 'tool_call', 'tool_result', 'note'], + }, + AgentSession: { + type: 'object', + properties: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], + }, + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], + }, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + dataSource: { + $ref: '#/$defs/AgentDataSource', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + id: { + type: 'string', + }, + title: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', + }, + }, + additionalProperties: false, + required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', + 'id', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', + ], + }, + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', + }, + }, + additionalProperties: false, + required: ['sessionId', 'position'], + }, + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', + oneOf: [ + { + description: 'Capture an empty history prefix.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], + }, + { + description: + 'Exclude the complete target Turn, including its submitted inputs.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], + }, + ], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', + }, + connectionId: { + type: 'string', + minLength: 1, + }, + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', + }, + selection: { + $ref: '#/$defs/AgentExecutorSelection', + }, + }, + additionalProperties: false, + required: ['kind', 'selection'], + }, + ], + }, + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { + type: 'object', + properties: { + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'namespace'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + AgentWorkspace: { + type: 'object', + properties: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], + }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { + type: 'string', + minLength: 1, + }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + }, + }, + additionalProperties: false, + required: ['path', 'directoryIdentity', 'identity'], + }, + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', + type: 'object', + properties: { + id: { + type: 'string', + }, + common_dir: { + type: 'string', + }, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', + }, + }, + additionalProperties: false, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], + }, + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], + }, + }, + }, + resources: {}, + }, + interaction: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentInteractionRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + requestId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId', 'requestId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'InteractionRecord', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + turnId: { + type: 'string', + }, + runId: { + type: 'string', + }, + requestId: { + type: 'string', + }, + createdAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + request: { + $ref: '#/$defs/InteractionRequest', + }, + outcome: { + anyOf: [ + { + $ref: '#/$defs/InteractionOutcome', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: [ + 'sessionId', + 'turnId', + 'runId', + 'requestId', + 'createdAt', + 'request', + 'outcome', + ], + $defs: { + InteractionRequest: { + oneOf: [ + { + description: + 'Human consent for the exact decision proposed for a child interaction.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session_approval', + }, + toolUseId: { + type: 'string', + }, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + sessionId: { + type: 'string', + }, + requestId: { + type: 'string', + }, + answer: { + $ref: '#/$defs/SessionApprovalAnswer', + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'basis', 'sessionId', 'requestId', 'answer'], + }, + { + description: + 'Consent for one already journaled service call; never a service grant.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'service_approval', + }, + toolUseId: { + type: 'string', + }, + toolName: { + type: 'string', + }, + service: { + type: 'string', + }, + method: { + type: 'string', + }, + binding: { + type: 'string', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: [ + 'kind', + 'toolUseId', + 'toolName', + 'service', + 'method', + 'binding', + 'through', + ], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'permissions', + }, + toolUseId: { + type: ['string', 'null'], + }, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + request: { + $ref: '#/$defs/PermissionRequest', + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'basis', 'request'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'question', + }, + toolUseId: { + type: 'string', + }, + questions: { + type: 'array', + items: { + $ref: '#/$defs/InteractionQuestion', + }, + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'questions'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'form', + }, + toolUseId: { + type: 'string', + }, + message: { + type: 'string', + }, + requester: { + $ref: '#/$defs/FormRequester', + }, + fields: { + type: 'array', + items: { + $ref: '#/$defs/FormField', + }, + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'message', 'requester', 'fields'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'client_capability', + }, + toolUseId: { + type: 'string', + }, + target: { + $ref: '#/$defs/GrantTarget', + }, + basis: { + description: + 'Agent-owned approvals expire with this original permission boundary.\nUnbound capabilities are explicit; Agent-owned capabilities capture a full binding stamp.', + anyOf: [ + { + $ref: '#/$defs/PermissionBasis', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'target'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'computer_app', + }, + toolUseId: { + type: 'string', + }, + nativeOperationId: { + type: 'string', + }, + target: { + $ref: '#/$defs/GrantTarget', + }, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + display: { + $ref: '#/$defs/ComputerAppDisplay', + }, + allowAlways: { + type: 'boolean', + }, + authorityRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: [ + 'kind', + 'toolUseId', + 'nativeOperationId', + 'target', + 'basis', + 'display', + 'allowAlways', + 'authorityRevision', + ], + }, + { + description: + 'Missing operating-system grants the waiting tool call needs. The producer\npolls the real grant state and commits `SystemPermissionGranted` itself;\nthe user can only cancel.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'system_permission', + }, + toolUseId: { + type: 'string', + }, + grants: { + description: 'Distinct missing grants.', + type: 'array', + items: { + $ref: '#/$defs/SystemPermissionGrant', + }, + minItems: 1, + }, + application: { + description: + 'The application name the operating system lists in its settings.', + type: 'string', + minLength: 1, + }, + applicationLocation: { + anyOf: [ + { + $ref: '#/$defs/SystemPermissionApplicationLocation', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'toolUseId', 'grants', 'application'], + }, + ], + }, + PermissionBasis: { + description: + 'Captured authority, never silently upgraded by resolving a current binding.', + type: 'object', + properties: { + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + }, + additionalProperties: false, + required: ['boundaryRevision', 'binding'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + SessionApprovalAnswer: { + description: + "Only access-expanding child decisions can require a parent's human confirmation.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'service_approval', + }, + decision: { + $ref: '#/$defs/ApprovalAllow', + }, + }, + additionalProperties: false, + required: ['kind', 'decision'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'permissions', + }, + decision: { + $ref: '#/$defs/PermissionApproval', + }, + }, + additionalProperties: false, + required: ['kind', 'decision'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'client_capability', + }, + decision: { + $ref: '#/$defs/ApprovalAllow', + }, + }, + additionalProperties: false, + required: ['kind', 'decision'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'computer_app', + }, + decision: { + $ref: '#/$defs/ComputerAppApproval', + }, + }, + additionalProperties: false, + required: ['kind', 'decision'], + }, + ], + }, + ApprovalAllow: { + type: 'string', + enum: ['allow'], + }, + PermissionApproval: { + oneOf: [ + { + type: 'object', + properties: { + decision: { + type: 'string', + const: 'allow', + }, + permissions: { + $ref: '#/$defs/Permissions', + }, + scope: { + $ref: '#/$defs/Scope2', + }, + }, + additionalProperties: false, + required: ['decision', 'permissions', 'scope'], + }, + ], + }, + Permissions: { + type: 'object', + properties: { + filesystem: { + type: 'array', + items: { + $ref: '#/$defs/Rule', + }, + }, + network: { + $ref: '#/$defs/Network', + }, + }, + additionalProperties: false, + required: ['filesystem', 'network'], + }, + Rule: { + description: + 'Paths have executor-native meaning. Validate and materialize them on the\nexecution host, never on a remote Desktop. This document alone grants nothing.', + type: 'object', + properties: { + path: { + type: 'string', + }, + scope: { + $ref: '#/$defs/Scope', + }, + access: { + $ref: '#/$defs/Access', + }, + }, + additionalProperties: false, + required: ['path', 'scope', 'access'], + }, + Scope: { + type: 'string', + enum: ['subtree', 'exact'], + }, + Access: { + description: + 'Equal-path conflict priority is different from authority breadth: deny wins.', + type: 'string', + enum: ['read', 'write', 'deny'], + }, + Network: { + oneOf: [ + { + type: 'string', + enum: ['denied', 'allowed'], + }, + { + type: 'object', + properties: { + restricted: { + type: 'object', + properties: { + destinations: { + type: 'array', + uniqueItems: true, + items: { + $ref: '#/$defs/Destination', + }, + }, + }, + additionalProperties: false, + required: ['destinations'], + }, + }, + required: ['restricted'], + additionalProperties: false, + }, + ], + }, + Destination: { + description: + 'Destination authority, independent of the upstream proxy used to reach it.\nA domain grant does not authorize its subdomains or other ports.', + type: 'object', + properties: { + host: { + type: 'string', + minLength: 1, + maxLength: 253, + }, + port: { + type: 'integer', + format: 'uint16', + minimum: 1, + maximum: 65535, + }, + }, + additionalProperties: false, + required: ['host', 'port'], + }, + Scope2: { + type: 'string', + enum: ['once', 'turn', 'session'], + }, + ComputerAppApproval: { + type: 'string', + enum: ['once', 'session', 'always'], + }, + PermissionRequest: { + description: + 'The Host supplies the real operation description; it is display information,\nnot authority inferred from a command string or a plugin-supplied Session ID.', + type: 'object', + properties: { + reason: { + type: 'string', + }, + command: { + anyOf: [ + { + $ref: '#/$defs/PermissionCommand', + }, + { + type: 'null', + }, + ], + }, + permissions: { + $ref: '#/$defs/Permissions', + }, + }, + additionalProperties: false, + required: ['reason', 'permissions'], + }, + PermissionCommand: { + type: 'object', + properties: { + command: { + type: 'string', + }, + cwd: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['command', 'cwd'], + }, + InteractionQuestion: { + type: 'object', + properties: { + question: { + type: 'string', + minLength: 1, + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/QuestionOption', + }, + minItems: 2, + maxItems: 3, + }, + }, + additionalProperties: false, + required: ['question', 'options'], + }, + QuestionOption: { + type: 'object', + properties: { + label: { + type: 'string', + minLength: 1, + }, + description: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['label'], + }, + FormRequester: { + type: 'object', + properties: { + name: { + type: 'string', + }, + source: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['name'], + }, + FormField: { + type: 'object', + properties: { + name: { + type: 'string', + }, + label: { + type: 'string', + }, + required: { + type: 'boolean', + }, + description: { + type: ['string', 'null'], + }, + }, + required: ['name', 'label', 'required'], + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'string', + }, + default: { + type: ['string', 'null'], + }, + minLength: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + maxLength: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + format: { + anyOf: [ + { + $ref: '#/$defs/FormFormat', + }, + { + type: 'null', + }, + ], + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'number', + }, + default: { + type: ['number', 'null'], + format: 'double', + }, + minimum: { + type: ['number', 'null'], + format: 'double', + }, + maximum: { + type: ['number', 'null'], + format: 'double', + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'integer', + }, + default: { + type: ['number', 'null'], + format: 'double', + }, + minimum: { + type: ['number', 'null'], + format: 'double', + }, + maximum: { + type: ['number', 'null'], + format: 'double', + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'boolean', + }, + default: { + type: ['boolean', 'null'], + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'single_select', + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/FormOption', + }, + }, + default: { + type: ['string', 'null'], + }, + }, + required: ['kind', 'options'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'multi_select', + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/FormOption', + }, + }, + default: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + minItems: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + maxItems: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + }, + required: ['kind', 'options'], + }, + ], + }, + FormFormat: { + type: 'string', + enum: ['email', 'uri', 'date', 'date-time'], + }, + FormOption: { + type: 'object', + properties: { + value: { + type: 'string', + }, + label: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['value', 'label'], + }, + GrantTarget: { + type: 'object', + properties: { + providerId: { + type: 'string', + }, + contractId: { + type: 'string', + }, + serverId: { + type: 'string', + }, + toolName: { + type: 'string', + }, + capability: { + $ref: '#/$defs/GrantCapability', + }, + scope: { + $ref: '#/$defs/GrantScope', + }, + }, + required: [ + 'providerId', + 'contractId', + 'serverId', + 'toolName', + 'capability', + 'scope', + ], + }, + GrantCapability: { + oneOf: [ + { + type: 'string', + enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], + }, + { + description: + 'File download consent for a browser origin, independent of page access.', + type: 'string', + const: 'browser_download', + }, + { + description: + "Read the browser profile's browsing history, independent of page access.", + type: 'string', + const: 'browser_history', + }, + { + description: + 'File upload consent for a browser origin, independent of reads and downloads.', + type: 'string', + const: 'browser_upload', + }, + ], + }, + GrantScope: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'browser_origin', + }, + origin: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'origin'], + }, + { + description: + 'One selected browser profile generation; never an origin or app-wide grant.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'browser_profile', + }, + browserId: { + type: 'string', + }, + generation: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'browserId', 'generation'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capability', + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'mcp_tool', + }, + serverId: { + type: 'string', + }, + toolName: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'serverId', 'toolName'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'computer_app', + }, + platform: { + $ref: '#/$defs/ComputerAppPlatform', + }, + applicationId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'platform', 'applicationId'], + }, + ], + }, + ComputerAppPlatform: { + type: 'string', + enum: ['macos', 'windows'], + }, + ComputerAppDisplay: { + type: 'object', + properties: { + name: { + type: 'string', + }, + appPath: { + type: 'string', + }, + risk: { + anyOf: [ + { + $ref: '#/$defs/ComputerAppRisk', + }, + { + type: 'null', + }, + ], + }, + warning: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['name', 'appPath'], + }, + ComputerAppRisk: { + type: 'string', + enum: ['low', 'high'], + }, + SystemPermissionGrant: { + description: + 'A macOS privacy grant (TCC) that the Host process itself needs before native\nComputer Use can run. This is operating-system state the user changes in\nSystem Settings, not a Maka grant; the Host observes it and commits the outcome.', + oneOf: [ + { + description: 'System Settings > Privacy & Security > Accessibility.', + type: 'string', + const: 'accessibility', + }, + { + description: + 'System Settings > Privacy & Security > Screen & System Audio Recording.', + type: 'string', + const: 'screen_recording', + }, + ], + }, + SystemPermissionApplicationLocation: { + type: 'object', + properties: { + fileUrl: { + type: 'string', + }, + stateDomain: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['fileUrl', 'stateDomain'], + }, + InteractionOutcome: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session_approval_decision', + }, + decision: { + $ref: '#/$defs/Decision', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'service_approval_decision', + }, + decision: { + $ref: '#/$defs/Decision', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'permissions_decision', + }, + decision: { + $ref: '#/$defs/Decision2', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'question_answer', + }, + answers: { + type: 'array', + items: { + type: ['string', 'null'], + }, + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'answers', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'form_answer', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + unevaluatedProperties: false, + oneOf: [ + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'accept', + }, + values: { + type: 'object', + additionalProperties: { + $ref: '#/$defs/FormValue', + }, + }, + }, + required: ['action', 'values'], + }, + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'decline', + }, + }, + required: ['action'], + }, + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'cancel', + }, + }, + required: ['action'], + }, + ], + required: ['kind', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'client_capability_decision', + }, + decision: { + $ref: '#/$defs/Decision', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'computer_app_decision', + }, + decision: { + $ref: '#/$defs/ComputerAppDecision', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + description: + 'Producer-committed once every requested system grant is present; no answer maps here.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'system_permission_granted', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'closure', + }, + reason: { + $ref: '#/$defs/ClosureReason', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'reason', 'committedAt'], + }, + ], + }, + Decision: { + type: 'string', + enum: ['allow', 'deny'], + }, + Decision2: { + oneOf: [ + { + type: 'object', + properties: { + decision: { + type: 'string', + const: 'deny', + }, + }, + required: ['decision'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + decision: { + type: 'string', + const: 'allow', + }, + permissions: { + $ref: '#/$defs/Permissions', + }, + scope: { + $ref: '#/$defs/Scope2', + }, + }, + additionalProperties: false, + required: ['decision', 'permissions', 'scope'], + }, + ], + }, + FormValue: { + anyOf: [ + { + type: 'string', + }, + { + type: 'number', + format: 'double', + }, + { + type: 'boolean', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + ComputerAppDecision: { + type: 'string', + enum: ['once', 'session', 'always', 'deny'], + }, + ClosureReason: { + type: 'string', + enum: [ + 'steered', + 'turn_stopped', + 'turn_terminal', + 'producer_cancelled', + 'timed_out', + 'host_restarted', + 'provider_disconnected', + ], + }, + }, + }, + resources: {}, + }, + list: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentList', + type: 'object', + properties: { + filter: { + $ref: '#/$defs/AgentSessionFilter', + }, + includeArchived: { + description: 'Include archived conversations alongside active ones.', + type: 'boolean', + default: false, + }, + rootsOnly: { + description: + 'Only conversations without a parent; omitted queries still include children.', + type: 'boolean', + default: false, + }, + position: { + $ref: '#/$defs/AgentSessionPosition', + }, + scope: { + $ref: '#/$defs/AgentSessionListScope', + default: { + kind: 'all', + }, + }, + titleQuery: { + description: + 'Literal, case-sensitive title substring; an empty query matches all titles.', + type: 'string', + default: '', + }, + }, + additionalProperties: false, + required: ['filter', 'position'], + $defs: { + AgentSessionFilter: { + type: 'string', + enum: ['all', 'archived', 'needs_you'], + }, + AgentSessionPosition: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'at', + }, + cursor: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'cursor'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'after', + }, + revision: { + type: 'string', + }, + cursor: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'revision', 'cursor'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before', + }, + revision: { + type: 'string', + }, + cursor: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'revision', 'cursor'], + }, + ], + }, + AgentSessionListScope: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'all', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'project', + }, + projectId: { + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['kind', 'projectId'], + }, + { + description: 'Direct children of an owned session, across workspaces.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'children', + }, + sessionId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId'], + }, + { + description: + 'Resolve the exact workspace from an owned session, never a caller path.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workspace', + }, + sessionId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId'], + }, + ], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionPage', + type: 'object', + properties: { + revision: { + type: 'string', + }, + needsYouCount: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + sessions: { + type: 'array', + items: { + $ref: '#/$defs/AgentSession', + }, + maxItems: 32, + }, + before: { + type: ['string', 'null'], + }, + after: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['revision', 'needsYouCount', 'sessions', 'before', 'after'], + $defs: { + AgentSession: { + type: 'object', + properties: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], + }, + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], + }, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + dataSource: { + $ref: '#/$defs/AgentDataSource', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + id: { + type: 'string', + }, + title: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', + }, + }, + additionalProperties: false, + required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', + 'id', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', + ], + }, + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', + }, + }, + additionalProperties: false, + required: ['sessionId', 'position'], + }, + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', oneOf: [ + { + description: 'Capture an empty history prefix.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], + }, { description: - 'Human consent for the exact decision proposed for a child interaction.', + 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'session_approval', + const: 'before_turn', }, - toolUseId: { + turnId: { type: 'string', + minLength: 1, }, - basis: { - $ref: '#/$defs/PermissionBasis', + }, + additionalProperties: false, + required: ['kind', 'turnId'], + }, + ], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', }, - sessionId: { + connectionId: { type: 'string', + minLength: 1, }, - requestId: { + modelId: { type: 'string', + minLength: 1, }, - answer: { - $ref: '#/$defs/SessionApprovalAnswer', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'toolUseId', 'basis', 'sessionId', 'requestId', 'answer'], + required: ['kind', 'connectionId', 'modelId'], }, { - description: - 'Consent for one already journaled service call; never a service grant.', type: 'object', properties: { kind: { type: 'string', - const: 'service_approval', - }, - toolUseId: { - type: 'string', + const: 'executor', }, - toolName: { - type: 'string', + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, - service: { - type: 'string', + }, + additionalProperties: false, + required: ['kind', 'selection'], + }, + ], + }, + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { + type: 'object', + properties: { + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', }, - method: { - type: 'string', + { + type: 'null', }, - binding: { - type: 'string', + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'namespace'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + AgentWorkspace: { + type: 'object', + properties: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'null', }, - }, - additionalProperties: false, - required: [ - 'kind', - 'toolUseId', - 'toolName', - 'service', - 'method', - 'binding', - 'through', ], }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { + type: 'string', + minLength: 1, + }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + }, + }, + additionalProperties: false, + required: ['path', 'directoryIdentity', 'identity'], + }, + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', + type: 'object', + properties: { + id: { + type: 'string', + }, + common_dir: { + type: 'string', + }, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', + }, + }, + additionalProperties: false, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], + }, + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], + }, + }, + }, + resources: {}, + }, + listFiles: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentFileListRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + path: { + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'permissions', - }, - toolUseId: { - type: ['string', 'null'], - }, - basis: { - $ref: '#/$defs/PermissionBasis', - }, - request: { - $ref: '#/$defs/PermissionRequest', - }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'basis', 'request'], + $ref: '#/$defs/WorkspacePath', }, { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'question', - }, - toolUseId: { - type: 'string', - }, - questions: { - type: 'array', - items: { - $ref: '#/$defs/InteractionQuestion', - }, - }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'questions'], + type: 'null', }, + ], + }, + after: { + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'form', - }, - toolUseId: { - type: 'string', - }, - message: { - type: 'string', - }, - requester: { - $ref: '#/$defs/FormRequester', - }, - fields: { - type: 'array', - items: { - $ref: '#/$defs/FormField', - }, - }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'message', 'requester', 'fields'], + $ref: '#/$defs/AgentFileCursor', }, { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'client_capability', - }, - toolUseId: { - type: 'string', - }, - target: { - $ref: '#/$defs/GrantTarget', - }, - basis: { - description: - 'Agent-owned approvals expire with this original permission boundary.\nUnbound capabilities are explicit; Agent-owned capabilities capture a full binding stamp.', - anyOf: [ - { - $ref: '#/$defs/PermissionBasis', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'target'], + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['sessionId', 'path', 'after'], + $defs: { + WorkspacePath: { + type: 'object', + properties: { + bytesBase64: { + type: 'string', }, + }, + additionalProperties: false, + required: ['bytesBase64'], + }, + AgentFileCursor: { + type: 'object', + properties: { + directoryIdentity: { + type: 'string', + }, + after: { + $ref: '#/$defs/WorkspacePath', + }, + }, + additionalProperties: false, + required: ['directoryIdentity', 'after'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentFilePage', + type: 'object', + properties: { + directoryIdentity: { + type: 'string', + }, + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentFileEntry', + }, + }, + next: { + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'computer_app', - }, - toolUseId: { - type: 'string', - }, - nativeOperationId: { - type: 'string', - }, - target: { - $ref: '#/$defs/GrantTarget', - }, - basis: { - $ref: '#/$defs/PermissionBasis', - }, - display: { - $ref: '#/$defs/ComputerAppDisplay', - }, - allowAlways: { - type: 'boolean', - }, - authorityRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: [ - 'kind', - 'toolUseId', - 'nativeOperationId', - 'target', - 'basis', - 'display', - 'allowAlways', - 'authorityRevision', - ], + $ref: '#/$defs/AgentFileCursor', }, { - description: - 'Missing operating-system grants the waiting tool call needs. The producer\npolls the real grant state and commits `SystemPermissionGranted` itself;\nthe user can only cancel.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'system_permission', - }, - toolUseId: { - type: 'string', - }, - grants: { - description: 'Distinct missing grants.', - type: 'array', - items: { - $ref: '#/$defs/SystemPermissionGrant', - }, - minItems: 1, - }, - application: { - description: - 'The application name the operating system lists in its settings.', - type: 'string', - minLength: 1, - }, - applicationLocation: { - anyOf: [ - { - $ref: '#/$defs/SystemPermissionApplicationLocation', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'grants', 'application'], + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['directoryIdentity', 'entries', 'next'], + $defs: { + AgentFileEntry: { + type: 'object', + properties: { + path: { + $ref: '#/$defs/WorkspacePath', }, - ], + kind: { + $ref: '#/$defs/AgentFileKind', + }, + }, + additionalProperties: false, + required: ['path', 'kind'], }, - PermissionBasis: { - description: - 'Captured authority, never silently upgraded by resolving a current binding.', + WorkspacePath: { type: 'object', properties: { - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, + bytesBase64: { + type: 'string', }, - binding: { - $ref: '#/$defs/BindingStamp', + }, + additionalProperties: false, + required: ['bytesBase64'], + }, + AgentFileKind: { + type: 'string', + enum: ['file', 'directory', 'symlink', 'other'], + }, + AgentFileCursor: { + type: 'object', + properties: { + directoryIdentity: { + type: 'string', + }, + after: { + $ref: '#/$defs/WorkspacePath', }, }, additionalProperties: false, - required: ['boundaryRevision', 'binding'], + required: ['directoryIdentity', 'after'], }, - BindingStamp: { + }, + }, + resources: {}, + }, + loadHistoryCheckpoint: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryCheckpointRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + revision: { + description: + 'Only the exact currently retained revision is readable; no latest fallback.', + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['sessionId', 'revision'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCheckpointLoad', + type: 'object', + properties: { + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, + state: { + $ref: '#/$defs/AgentCheckpointLoadState', + }, + }, + additionalProperties: false, + required: ['head', 'inspection', 'state'], + $defs: { + AgentHistoryCheckpointHead: { type: 'object', properties: { - id: { + revision: { type: 'string', format: 'uuid', }, - permissionEpoch: { + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', + }, + }, + additionalProperties: false, + required: ['revision', 'manifest'], + }, + AgentHistoryCheckpointObject: { + type: 'object', + properties: { + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', + }, + bytes: { type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['digest', 'bytes'], }, - SessionApprovalAnswer: { - description: - "Only access-expanding child decisions can require a parent's human confirmation.", + AgentCheckpointInspection: { + type: 'object', + properties: { + sourceSessionId: { + type: 'string', + minLength: 1, + }, + operationId: { + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['sourceSessionId', 'operationId'], + }, + AgentCheckpointLoadState: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'service_approval', - }, - decision: { - $ref: '#/$defs/ApprovalAllow', + const: 'preparing', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'decision'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'permissions', - }, - decision: { - $ref: '#/$defs/PermissionApproval', + const: 'ready', }, - }, - additionalProperties: false, - required: ['kind', 'decision'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'client_capability', + sessionIds: { + type: 'array', + items: { + type: 'string', + }, }, - decision: { - $ref: '#/$defs/ApprovalAllow', + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'sessionIds'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', - }, - decision: { - $ref: '#/$defs/ComputerAppApproval', - }, - }, - additionalProperties: false, - required: ['kind', 'decision'], - }, - ], - }, - ApprovalAllow: { - type: 'string', - enum: ['allow'], - }, - PermissionApproval: { - oneOf: [ - { - type: 'object', - properties: { - decision: { - type: 'string', - const: 'allow', - }, - permissions: { - $ref: '#/$defs/Permissions', - }, - scope: { - $ref: '#/$defs/Scope2', - }, - }, - additionalProperties: false, - required: ['decision', 'permissions', 'scope'], - }, - ], - }, - Permissions: { - type: 'object', - properties: { - filesystem: { - type: 'array', - items: { - $ref: '#/$defs/Rule', - }, - }, - network: { - $ref: '#/$defs/Network', - }, - }, - additionalProperties: false, - required: ['filesystem', 'network'], - }, - Rule: { - description: - 'Paths have executor-native meaning. Validate and materialize them on the\nexecution host, never on a remote Desktop. This document alone grants nothing.', - type: 'object', - properties: { - path: { - type: 'string', - }, - scope: { - $ref: '#/$defs/Scope', - }, - access: { - $ref: '#/$defs/Access', - }, - }, - additionalProperties: false, - required: ['path', 'scope', 'access'], - }, - Scope: { - type: 'string', - enum: ['subtree', 'exact'], - }, - Access: { - description: - 'Equal-path conflict priority is different from authority breadth: deny wins.', - type: 'string', - enum: ['read', 'write', 'deny'], - }, - Network: { - oneOf: [ - { - type: 'string', - enum: ['denied', 'allowed'], - }, - { - type: 'object', - properties: { - restricted: { - type: 'object', - properties: { - destinations: { - type: 'array', - uniqueItems: true, - items: { - $ref: '#/$defs/Destination', - }, - }, - }, - additionalProperties: false, - required: ['destinations'], + const: 'unavailable', }, }, - required: ['restricted'], + required: ['kind'], additionalProperties: false, }, ], - }, - Destination: { - description: - 'Destination authority, independent of the upstream proxy used to reach it.\nA domain grant does not authorize its subdomains or other ports.', + }, + AgentCheckpointWorkspaceSnapshot: { type: 'object', properties: { - host: { + identity: { type: 'string', - minLength: 1, - maxLength: 253, }, - port: { - type: 'integer', - format: 'uint16', - minimum: 1, - maximum: 65535, + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['host', 'port'], + required: ['identity', 'consistency', 'warning', 'inventory'], }, - Scope2: { + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', + }, + ], + }, + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', + type: 'string', + const: 'external_writers_not_fenced', + }, + ], + }, + }, + }, + resources: {}, + }, + loadedHistoryCheckpointArtifact: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCheckpointArtifactRead', + type: 'object', + properties: { + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, + sessionId: { type: 'string', - enum: ['once', 'turn', 'session'], + minLength: 1, }, - ComputerAppApproval: { + artifactId: { type: 'string', - enum: ['once', 'session', 'always'], + minLength: 1, }, - PermissionRequest: { - description: - 'The Host supplies the real operation description; it is display information,\nnot authority inferred from a command string or a plugin-supplied Session ID.', + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['inspection', 'sessionId', 'artifactId', 'offset'], + $defs: { + AgentCheckpointInspection: { type: 'object', properties: { - reason: { + sourceSessionId: { type: 'string', + minLength: 1, }, - command: { - anyOf: [ - { - $ref: '#/$defs/PermissionCommand', - }, - { - type: 'null', - }, - ], - }, - permissions: { - $ref: '#/$defs/Permissions', + operationId: { + type: 'string', + format: 'uuid', }, }, additionalProperties: false, - required: ['reason', 'permissions'], + required: ['sourceSessionId', 'operationId'], }, - PermissionCommand: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentCheckpointArtifactChunk', + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointArtifactChunk', + }, + { + type: 'null', + }, + ], + $defs: { + AgentCheckpointArtifactChunk: { type: 'object', properties: { - command: { - type: 'string', + artifact: { + $ref: '#/$defs/AgentCheckpointArtifact', }, - cwd: { + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + data: { + description: 'Base64, at most 32 KiB decoded. Empty only at the end.', type: 'string', + maxLength: 43692, }, }, additionalProperties: false, - required: ['command', 'cwd'], + required: ['artifact', 'offset', 'data'], }, - InteractionQuestion: { + AgentCheckpointArtifact: { type: 'object', properties: { - question: { + id: { type: 'string', - minLength: 1, }, - options: { - type: 'array', - items: { - $ref: '#/$defs/QuestionOption', - }, - minItems: 2, - maxItems: 3, + name: { + type: 'string', + }, + mimeType: { + type: ['string', 'null'], + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['question', 'options'], + required: ['id', 'name', 'bytes'], }, - QuestionOption: { + }, + }, + resources: {}, + }, + loadedHistoryCheckpointArtifacts: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCheckpointArtifactsRead', + type: 'object', + properties: { + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, + sessionId: { + type: 'string', + minLength: 1, + }, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['inspection', 'sessionId', 'offset'], + $defs: { + AgentCheckpointInspection: { type: 'object', properties: { - label: { + sourceSessionId: { type: 'string', minLength: 1, }, - description: { + operationId: { type: 'string', - minLength: 1, + format: 'uuid', }, }, additionalProperties: false, - required: ['label'], + required: ['sourceSessionId', 'operationId'], }, - FormRequester: { - type: 'object', - properties: { - name: { - type: 'string', - }, - source: { - type: ['string', 'null'], - }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCheckpointArtifacts', + type: 'object', + properties: { + artifacts: { + type: 'array', + items: { + $ref: '#/$defs/AgentCheckpointArtifact', }, - additionalProperties: false, - required: ['name'], }, - FormField: { + nextOffset: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['artifacts', 'nextOffset'], + $defs: { + AgentCheckpointArtifact: { type: 'object', properties: { - name: { + id: { type: 'string', }, - label: { + name: { type: 'string', }, - required: { - type: 'boolean', - }, - description: { + mimeType: { type: ['string', 'null'], }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, }, - required: ['name', 'label', 'required'], - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'string', - }, - default: { - type: ['string', 'null'], - }, - minLength: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - maxLength: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - format: { - anyOf: [ - { - $ref: '#/$defs/FormFormat', - }, - { - type: 'null', - }, - ], + additionalProperties: false, + required: ['id', 'name', 'bytes'], + }, + }, + }, + resources: {}, + }, + loadedHistoryCheckpointHistory: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCheckpointHistoryRead', + type: 'object', + properties: { + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, + sessionId: { + type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/HistoryPosition', + }, + includeThinking: { + type: 'boolean', + default: false, + }, + }, + additionalProperties: false, + required: ['inspection', 'sessionId', 'position'], + $defs: { + AgentCheckpointInspection: { + type: 'object', + properties: { + sourceSessionId: { + type: 'string', + minLength: 1, + }, + operationId: { + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['sourceSessionId', 'operationId'], + }, + HistoryPosition: { + description: + 'Row reveals an exact field; Before/After continue at byte boundaries and\nEvent addresses the raw event log. Returned chunks retain ascending source\norder, including a reverse window.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', }, }, required: ['kind'], + additionalProperties: false, }, { type: 'object', properties: { kind: { type: 'string', - const: 'number', - }, - default: { - type: ['number', 'null'], - format: 'double', - }, - minimum: { - type: ['number', 'null'], - format: 'double', - }, - maximum: { - type: ['number', 'null'], - format: 'double', + const: 'tail', }, }, required: ['kind'], + additionalProperties: false, }, { + description: + 'Reveal this exact row field from its beginning, including empty tool rows.', type: 'object', properties: { kind: { type: 'string', - const: 'integer', - }, - default: { - type: ['number', 'null'], - format: 'double', + const: 'row', }, - minimum: { - type: ['number', 'null'], - format: 'double', + position: { + $ref: '#/$defs/HistoryRowPosition', }, - maximum: { - type: ['number', 'null'], - format: 'double', + field: { + $ref: '#/$defs/HistoryField', }, }, - required: ['kind'], + additionalProperties: false, + required: ['kind', 'position', 'field'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'boolean', + const: 'before', }, - default: { - type: ['boolean', 'null'], + cursor: { + $ref: '#/$defs/Cursor', }, }, - required: ['kind'], + additionalProperties: false, + required: ['kind', 'cursor'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'single_select', - }, - options: { - type: 'array', - items: { - $ref: '#/$defs/FormOption', - }, + const: 'after', }, - default: { - type: ['string', 'null'], + cursor: { + $ref: '#/$defs/Cursor', }, }, - required: ['kind', 'options'], + additionalProperties: false, + required: ['kind', 'cursor'], }, { + description: + 'Locate canonical output at an execution event, including sparse rows.', type: 'object', properties: { kind: { type: 'string', - const: 'multi_select', + const: 'event', }, - options: { - type: 'array', - items: { - $ref: '#/$defs/FormOption', - }, + sequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - default: { - type: ['array', 'null'], - items: { - type: 'string', - }, + }, + additionalProperties: false, + required: ['kind', 'sequence'], + }, + ], + }, + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + type: 'object', + properties: { + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sourceSequence', 'itemIndex'], + }, + HistoryField: { + description: 'A readable field in a conversation window, in presentation order.', + type: 'string', + enum: ['thinking', 'body'], + }, + Cursor: { + description: + "Byte offset in one message field's UTF-8 text, not a bearer capability.", + type: 'object', + properties: { + field: { + $ref: '#/$defs/HistoryField', + default: 'body', + }, + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['position', 'offset'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryPage', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'ready', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + chunks: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryChunk', + }, + }, + before: { + anyOf: [ + { + $ref: '#/$defs/Cursor', }, - minItems: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, + { + type: 'null', + }, + ], + }, + after: { + anyOf: [ + { + $ref: '#/$defs/Cursor', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'through', 'chunks', 'before', 'after'], + }, + ], + $defs: { + AgentHistoryChunk: { + type: 'object', + properties: { + modelPart: { + anyOf: [ + { + $ref: '#/$defs/ModelPartIdentity', + }, + { + type: 'null', + }, + ], + }, + hasReferences: { + description: + 'The original user input has quotes, directory references or inline references.\nPresent only on the first fragment; display text cannot reconstruct these references.', + type: 'boolean', + }, + finalOutput: { + description: + "Part of the successfully completed native Turn's final output at `through`.\nIncludes all body parts and source citations; never a Thinking field.", + type: 'boolean', + }, + model: { + anyOf: [ + { + $ref: '#/$defs/HistoryModel', + }, + { + type: 'null', + }, + ], + }, + origin: { + anyOf: [ + { + $ref: '#/$defs/MessageOrigin', + }, + { + type: 'null', + }, + ], + }, + compaction: { + anyOf: [ + { + $ref: '#/$defs/HistoryCompaction', }, - maxItems: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, + { + type: 'null', }, + ], + }, + attachments: { + description: + 'Display metadata only, on the first user-message fragment; no storage capability.', + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryAttachment', }, - required: ['kind', 'options'], }, - ], - }, - FormFormat: { - type: 'string', - enum: ['email', 'uri', 'date', 'date-time'], - }, - FormOption: { - type: 'object', - properties: { - value: { - type: 'string', + timing: { + anyOf: [ + { + $ref: '#/$defs/HistoryTurnTiming', + }, + { + type: 'null', + }, + ], }, - label: { - type: 'string', + field: { + $ref: '#/$defs/HistoryField', }, - }, - additionalProperties: false, - required: ['value', 'label'], - }, - GrantTarget: { - type: 'object', - properties: { - providerId: { + messageId: { type: 'string', }, - contractId: { + turnId: { type: 'string', }, - serverId: { - type: 'string', + timestamp: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - toolName: { - type: 'string', + role: { + $ref: '#/$defs/Role', }, - capability: { - $ref: '#/$defs/GrantCapability', + position: { + $ref: '#/$defs/HistoryRowPosition', }, - scope: { - $ref: '#/$defs/GrantScope', + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + totalBytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + text: { + type: 'string', + }, + tool: { + anyOf: [ + { + $ref: '#/$defs/HistoryTool', + }, + { + type: 'null', + }, + ], }, }, + additionalProperties: false, required: [ - 'providerId', - 'contractId', - 'serverId', - 'toolName', - 'capability', - 'scope', + 'field', + 'messageId', + 'turnId', + 'timestamp', + 'role', + 'position', + 'offset', + 'totalBytes', + 'text', + 'tool', ], }, - GrantCapability: { - oneOf: [ - { - type: 'string', - enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], - }, - { - description: - 'File download consent for a browser origin, independent of page access.', - type: 'string', - const: 'browser_download', - }, - { - description: - "Read the browser profile's browsing history, independent of page access.", + ModelPartIdentity: { + type: 'object', + properties: { + stepId: { type: 'string', - const: 'browser_history', }, - { - description: - 'File upload consent for a browser origin, independent of reads and downloads.', - type: 'string', - const: 'browser_upload', + ordinal: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - ], + }, + additionalProperties: false, + required: ['stepId', 'ordinal'], }, - GrantScope: { + HistoryModel: { + description: 'Model identity recorded for the exact historical assistant step.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'browser_origin', - }, - origin: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'origin'], - }, - { - description: - 'One selected browser profile generation; never an origin or app-wide grant.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'browser_profile', - }, - browserId: { - type: 'string', - }, - generation: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'browserId', 'generation'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'capability', - }, - }, - additionalProperties: false, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'mcp_tool', + const: 'native', }, - serverId: { + requestedModelId: { type: 'string', }, - toolName: { - type: 'string', + reportedModelId: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['kind', 'serverId', 'toolName'], + required: ['kind', 'requestedModelId', 'reportedModelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', - }, - platform: { - $ref: '#/$defs/ComputerAppPlatform', + const: 'imported', }, - applicationId: { + modelId: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'platform', 'applicationId'], - }, - ], - }, - ComputerAppPlatform: { - type: 'string', - enum: ['macos', 'windows'], - }, - ComputerAppDisplay: { - type: 'object', - properties: { - name: { - type: 'string', - }, - appPath: { - type: 'string', - }, - risk: { - anyOf: [ - { - $ref: '#/$defs/ComputerAppRisk', - }, - { - type: 'null', - }, - ], - }, - warning: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['name', 'appPath'], - }, - ComputerAppRisk: { - type: 'string', - enum: ['low', 'high'], - }, - SystemPermissionGrant: { - description: - 'A macOS privacy grant (TCC) that the Host process itself needs before native\nComputer Use can run. This is operating-system state the user changes in\nSystem Settings, not a Maka grant; the Host observes it and commits the outcome.', - oneOf: [ - { - description: 'System Settings > Privacy & Security > Accessibility.', - type: 'string', - const: 'accessibility', - }, - { - description: - 'System Settings > Privacy & Security > Screen & System Audio Recording.', - type: 'string', - const: 'screen_recording', + required: ['kind', 'modelId'], }, ], }, - SystemPermissionApplicationLocation: { - type: 'object', - properties: { - fileUrl: { - type: 'string', - }, - stateDomain: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['fileUrl', 'stateDomain'], - }, - InteractionOutcome: { + MessageOrigin: { + description: + 'Trusted producer attribution, preserved independently of editable content.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'session_approval_decision', - }, - decision: { - $ref: '#/$defs/Decision', + const: 'session', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + invocation: { + $ref: '#/$defs/Invocation', }, }, additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + required: ['kind', 'invocation'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'service_approval_decision', + const: 'goal', }, - decision: { - $ref: '#/$defs/Decision', + goalId: { + type: 'string', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + objective: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + required: ['kind', 'goalId', 'objective'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'permissions_decision', - }, - decision: { - $ref: '#/$defs/Decision2', + const: 'workhub_task', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + taskId: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + required: ['kind', 'taskId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'question_answer', + const: 'workhub_graph', }, - answers: { - type: 'array', - items: { - type: ['string', 'null'], - }, + graphId: { + type: 'string', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + }, + additionalProperties: false, + required: ['kind', 'graphId'], + }, + { + description: 'A worker conversation created by the Bot finished a run.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'bot_worker', + }, + sessionId: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'answers', 'committedAt'], + required: ['kind', 'sessionId'], }, { + description: + "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", type: 'object', properties: { kind: { type: 'string', - const: 'form_answer', + const: 'plan', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + executionId: { + type: 'string', + }, + title: { + type: 'string', }, }, - unevaluatedProperties: false, - oneOf: [ + additionalProperties: false, + required: ['kind', 'executionId', 'title'], + }, + ], + }, + Invocation: { + type: 'object', + properties: { + session_id: { + type: 'string', + }, + turn_id: { + type: 'string', + }, + run_id: { + type: 'string', + }, + invocation_id: { + type: 'string', + }, + }, + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], + }, + HistoryCompaction: { + description: + 'Duration of a successfully adopted context checkpoint, in recorded milliseconds.', + type: 'object', + properties: { + durationMs: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['durationMs'], + }, + AgentHistoryAttachment: { + type: 'object', + properties: { + name: { + type: 'string', + }, + mimeType: { + type: 'string', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + kind: { + $ref: '#/$defs/AttachmentKind', + }, + }, + additionalProperties: false, + required: ['name', 'mimeType', 'bytes', 'kind'], + }, + AttachmentKind: { + type: 'string', + enum: ['image', 'pdf', 'doc', 'code', 'other'], + }, + HistoryTurnTiming: { + description: + 'Recorded logical Turn boundaries at the same history fence. An unfinished\nhistorical boundary alone does not establish that execution is still live.', + type: 'object', + properties: { + startedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + finished: { + anyOf: [ { - type: 'object', - properties: { - action: { - type: 'string', - const: 'accept', - }, - values: { - type: 'object', - additionalProperties: { - $ref: '#/$defs/FormValue', - }, - }, - }, - required: ['action', 'values'], + $ref: '#/$defs/HistoryTurnFinished', + }, + { + type: 'null', }, + ], + }, + }, + additionalProperties: false, + required: ['startedAt', 'finished'], + }, + HistoryTurnFinished: { + type: 'object', + properties: { + at: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + outcome: { + $ref: '#/$defs/HistoryTurnOutcome', + }, + }, + additionalProperties: false, + required: ['at', 'outcome'], + }, + HistoryTurnOutcome: { + type: 'string', + enum: ['completed', 'failed', 'aborted'], + }, + HistoryField: { + description: 'A readable field in a conversation window, in presentation order.', + type: 'string', + enum: ['thinking', 'body'], + }, + Role: { + description: + 'Canonical message author/kind; the field identifies body versus thinking.\nTool body JSON contributes string values, never its keys.', + type: 'string', + enum: ['compaction', 'failure', 'user', 'assistant', 'tool_call', 'tool_result'], + }, + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + type: 'object', + properties: { + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sourceSequence', 'itemIndex'], + }, + HistoryTool: { + description: + 'Canonical references at the requested fence, never execution authority.\nBoth references are present only for a unique same-Turn call/result pair\nwith matching origin and parentage. An unpaired row retains its own reference.', + type: 'object', + properties: { + toolUseId: { + type: 'string', + }, + call: { + anyOf: [ { - type: 'object', - properties: { - action: { - type: 'string', - const: 'decline', - }, - }, - required: ['action'], + $ref: '#/$defs/HistoryToolCall', }, { - type: 'object', - properties: { - action: { - type: 'string', - const: 'cancel', - }, - }, - required: ['action'], + type: 'null', }, ], - required: ['kind', 'committedAt'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'client_capability_decision', - }, - decision: { - $ref: '#/$defs/Decision', + result: { + anyOf: [ + { + $ref: '#/$defs/HistoryToolResult', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + ], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'computer_app_decision', - }, - decision: { - $ref: '#/$defs/ComputerAppDecision', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, + }, + additionalProperties: false, + required: ['toolUseId', 'call', 'result'], + }, + HistoryToolCall: { + type: 'object', + properties: { + ancestorToolUseIds: { + description: + 'Recorded ancestors in this Turn, nearest first; display references only.', + type: 'array', + items: { + type: 'string', }, - additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], }, - { + presentation: { description: - 'Producer-committed once every requested system grant is present; no answer maps here.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'system_permission_granted', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'committedAt'], + 'Opaque display contract captured at dispatch; no execution authority.', + type: ['string', 'null'], + default: null, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'closure', - }, - reason: { - $ref: '#/$defs/ClosureReason', + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + name: { + type: 'string', + }, + title: { + anyOf: [ + { + $ref: '#/$defs/DisplayText', }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'reason', 'committedAt'], + ], + }, + pending: { + description: + 'This native Turn is still open at the history fence; not execution authority.', + type: 'boolean', + }, + dispatched: { + description: + 'A unique original Host dispatch is recorded at this fence; not liveness.', + type: 'boolean', }, + }, + additionalProperties: false, + required: [ + 'ancestorToolUseIds', + 'position', + 'name', + 'title', + 'pending', + 'dispatched', ], }, - Decision: { - type: 'string', - enum: ['allow', 'deny'], - }, - Decision2: { - oneOf: [ - { + DisplayText: { + description: + 'Presentation text is distinct from the stable identity used for navigation.', + type: 'object', + properties: { + fallback: { + type: 'string', + }, + translations: { type: 'object', - properties: { - decision: { - type: 'string', - const: 'deny', - }, + additionalProperties: { + type: 'string', }, - required: ['decision'], - additionalProperties: false, + default: {}, }, - { - type: 'object', - properties: { - decision: { - type: 'string', - const: 'allow', - }, - permissions: { - $ref: '#/$defs/Permissions', + }, + additionalProperties: false, + required: ['fallback'], + }, + HistoryToolResult: { + type: 'object', + properties: { + settlement: { + anyOf: [ + { + $ref: '#/$defs/ToolSettlement', }, - scope: { - $ref: '#/$defs/Scope2', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['decision', 'permissions', 'scope'], + ], }, - ], - }, - FormValue: { - anyOf: [ - { - type: 'string', + position: { + $ref: '#/$defs/HistoryRowPosition', }, - { - type: 'number', - format: 'double', + isError: { + description: 'Does not distinguish failed from unknown effects.', + type: 'boolean', }, - { + image: { + description: + 'Canonical content is an image; this does not grant artifact reads.', type: 'boolean', }, - { - type: 'array', - items: { - type: 'string', - }, + images: { + description: + 'Images the model received beside non-image content, readable by index.', + type: 'integer', + format: 'uint32', + minimum: 0, }, - ], + }, + additionalProperties: false, + required: ['settlement', 'position', 'isError', 'image', 'images'], }, - ComputerAppDecision: { + ToolSettlement: { + description: + 'Recorded Host settlement for presentation; returned is not business success.\nForeign/provider results do not establish this Host evidence.', type: 'string', - enum: ['once', 'session', 'always', 'deny'], + enum: ['returned', 'failed', 'unknown', 'rejected'], }, - ClosureReason: { - type: 'string', - enum: [ - 'steered', - 'turn_stopped', - 'turn_terminal', - 'producer_cancelled', - 'timed_out', - 'host_restarted', - 'provider_disconnected', - ], + Cursor: { + description: + "Byte offset in one message field's UTF-8 text, not a bearer capability.", + type: 'object', + properties: { + field: { + $ref: '#/$defs/HistoryField', + default: 'body', + }, + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['position', 'offset'], }, }, }, resources: {}, }, - list: { - mode: 'query', + manageMcpServer: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentList', + title: 'AgentMcpConfigurationChange', type: 'object', properties: { - filter: { - $ref: '#/$defs/AgentSessionFilter', - }, - includeArchived: { - description: 'Include archived conversations alongside active ones.', - type: 'boolean', - default: false, - }, - rootsOnly: { - description: - 'Only conversations without a parent; omitted queries still include children.', - type: 'boolean', - default: false, + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentWorkspace', + }, + { + type: 'null', + }, + ], }, - position: { - $ref: '#/$defs/AgentSessionPosition', + location: { + $ref: '#/$defs/ConfigurationLocation', }, - scope: { - $ref: '#/$defs/AgentSessionListScope', - default: { - kind: 'all', - }, + name: { + type: 'string', }, - titleQuery: { - description: - 'Literal, case-sensitive title substring; an empty query matches all titles.', + revision: { type: 'string', - default: '', + }, + change: { + $ref: '#/$defs/AgentMcpServerChange', }, }, additionalProperties: false, - required: ['filter', 'position'], + required: ['location', 'name', 'revision', 'change'], $defs: { - AgentSessionFilter: { - type: 'string', - enum: ['all', 'archived', 'needs_you'], - }, - AgentSessionPosition: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'at', + AgentWorkspace: { + type: 'object', + properties: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', }, - cursor: { - type: 'string', - minLength: 1, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'cursor'], + ], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'after', - }, - revision: { - type: 'string', - }, - cursor: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'revision', 'cursor'], + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before', - }, - revision: { - type: 'string', - }, - cursor: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'revision', 'cursor'], + path: { + type: 'string', + minLength: 1, + }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + }, + }, + additionalProperties: false, + required: ['path', 'directoryIdentity', 'identity'], + }, + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', + type: 'object', + properties: { + id: { + type: 'string', + }, + common_dir: { + type: 'string', + }, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', + }, + }, + additionalProperties: false, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', ], }, - AgentSessionListScope: { + ConfigurationLocation: { + description: + 'Standard user/project configuration locations, ordered from highest precedence.', + type: 'string', + enum: ['project:maka', 'project:agents', 'user:maka', 'user:agents'], + }, + AgentMcpServerChange: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'all', + const: 'save', }, + configuration: true, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'configuration'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'project', - }, - projectId: { - type: 'string', - format: 'uuid', + const: 'delete', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'projectId'], }, - { - description: 'Direct children of an owned session, across workspaces.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'children', - }, - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], + ], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMcpConfigurationResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'saved', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'changed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', }, - { - description: - 'Resolve the exact workspace from an owned session, never a caller path.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'workspace', - }, - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], + reason: { + type: 'string', }, - ], + }, + additionalProperties: false, + required: ['kind', 'reason'], }, - }, + ], }, - output: { + resources: {}, + }, + markRead: { + mode: 'mutation', + input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionPage', + title: 'AgentMarkRead', type: 'object', properties: { - revision: { + sessionId: { type: 'string', + minLength: 1, }, - needsYouCount: { - type: 'integer', - format: 'uint64', - minimum: 0, + messageId: { + type: 'string', + minLength: 1, }, - sessions: { - type: 'array', - items: { - $ref: '#/$defs/AgentSession', + }, + additionalProperties: false, + required: ['sessionId', 'messageId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMarkReadResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'observed', + }, + session: { + $ref: '#/$defs/AgentSession', + }, }, - maxItems: 32, - }, - before: { - type: ['string', 'null'], + additionalProperties: false, + required: ['kind', 'session'], }, - after: { - type: ['string', 'null'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', + }, + reason: { + $ref: '#/$defs/AgentRejection', + }, + }, + additionalProperties: false, + required: ['kind', 'reason'], }, - }, - additionalProperties: false, - required: ['revision', 'needsYouCount', 'sessions', 'before', 'after'], + ], $defs: { AgentSession: { type: 'object', @@ -12061,146 +14577,30 @@ export const contracts = { type: 'string', enum: ['idle', 'working', 'waiting'], }, - }, - }, - resources: {}, - }, - listFiles: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentFileListRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - path: { - anyOf: [ - { - $ref: '#/$defs/WorkspacePath', - }, - { - type: 'null', - }, - ], - }, - after: { - anyOf: [ - { - $ref: '#/$defs/AgentFileCursor', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['sessionId', 'path', 'after'], - $defs: { - WorkspacePath: { - type: 'object', - properties: { - bytesBase64: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['bytesBase64'], - }, - AgentFileCursor: { - type: 'object', - properties: { - directoryIdentity: { - type: 'string', - }, - after: { - $ref: '#/$defs/WorkspacePath', - }, - }, - additionalProperties: false, - required: ['directoryIdentity', 'after'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentFilePage', - type: 'object', - properties: { - directoryIdentity: { + AgentRejection: { type: 'string', - }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentFileEntry', - }, - }, - next: { - anyOf: [ - { - $ref: '#/$defs/AgentFileCursor', - }, - { - type: 'null', - }, + enum: [ + 'permission_change_requires_user', + 'title_invalid', + 'labels_invalid', + 'tools_unavailable', + 'busy', + 'workspace_unavailable', + 'session_unavailable', + 'history_unavailable', + 'managed_session', + 'closed', ], }, }, - additionalProperties: false, - required: ['directoryIdentity', 'entries', 'next'], - $defs: { - AgentFileEntry: { - type: 'object', - properties: { - path: { - $ref: '#/$defs/WorkspacePath', - }, - kind: { - $ref: '#/$defs/AgentFileKind', - }, - }, - additionalProperties: false, - required: ['path', 'kind'], - }, - WorkspacePath: { - type: 'object', - properties: { - bytesBase64: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['bytesBase64'], - }, - AgentFileKind: { - type: 'string', - enum: ['file', 'directory', 'symlink', 'other'], - }, - AgentFileCursor: { - type: 'object', - properties: { - directoryIdentity: { - type: 'string', - }, - after: { - $ref: '#/$defs/WorkspacePath', - }, - }, - additionalProperties: false, - required: ['directoryIdentity', 'after'], - }, - }, }, resources: {}, }, - manageMcpServer: { - mode: 'mutation', + mcpConfigurations: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpConfigurationChange', + title: 'AgentMcpConfigurations', type: 'object', properties: { workspace: { @@ -12210,24 +14610,11 @@ export const contracts = { }, { type: 'null', - }, - ], - }, - location: { - $ref: '#/$defs/ConfigurationLocation', - }, - name: { - type: 'string', - }, - revision: { - type: 'string', - }, - change: { - $ref: '#/$defs/AgentMcpServerChange', + }, + ], }, }, additionalProperties: false, - required: ['location', 'name', 'revision', 'change'], $defs: { AgentWorkspace: { type: 'object', @@ -12299,114 +14686,169 @@ export const contracts = { 'directory', ], }, - ConfigurationLocation: { - description: - 'Standard user/project configuration locations, ordered from highest precedence.', - type: 'string', - enum: ['project:maka', 'project:agents', 'user:maka', 'user:agents'], - }, - AgentMcpServerChange: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'save', - }, - configuration: true, - }, - additionalProperties: false, - required: ['kind', 'configuration'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'delete', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - ], - }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpConfigurationResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'saved', - }, + title: 'AgentMcpConfigurationList', + type: 'object', + properties: { + locations: { + type: 'array', + items: { + $ref: '#/$defs/AgentMcpConfigurationLocation', }, - required: ['kind'], - additionalProperties: false, }, - { + }, + additionalProperties: false, + required: ['locations'], + $defs: { + AgentMcpConfigurationLocation: { type: 'object', properties: { - kind: { + location: { + $ref: '#/$defs/ConfigurationLocation', + }, + path: { type: 'string', - const: 'changed', + }, + revision: { + type: 'string', + }, + servers: { + type: 'array', + items: { + $ref: '#/$defs/AgentMcpConfigurationEntry', + }, }, }, - required: ['kind'], additionalProperties: false, + required: ['location', 'path', 'revision', 'servers'], }, - { + ConfigurationLocation: { + description: + 'Standard user/project configuration locations, ordered from highest precedence.', + type: 'string', + enum: ['project:maka', 'project:agents', 'user:maka', 'user:agents'], + }, + AgentMcpConfigurationEntry: { type: 'object', properties: { - kind: { + name: { type: 'string', - const: 'rejected', }, - reason: { - type: 'string', + configuration: true, + shadowedBy: { + anyOf: [ + { + $ref: '#/$defs/ConfigurationLocation', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['name', 'configuration'], }, - ], + }, }, resources: {}, }, - markRead: { + metadata: { mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMarkRead', + title: 'AgentMetadata', type: 'object', properties: { + pinned: { + description: "Omission preserves the conversation's position in the catalog.", + type: ['boolean', 'null'], + default: null, + }, + permissions: { + description: 'Omission leaves the permission boundary and its approvals unchanged.', + anyOf: [ + { + $ref: '#/$defs/AgentPermissionChange', + }, + { + type: 'null', + }, + ], + }, sessionId: { type: 'string', minLength: 1, }, - messageId: { + expectedRevision: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + title: { type: 'string', minLength: 1, }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, }, additionalProperties: false, - required: ['sessionId', 'messageId'], + required: ['sessionId', 'expectedRevision', 'title', 'labels'], + $defs: { + AgentPermissionChange: { + type: 'object', + properties: { + settings: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + revokeGrants: { + description: + 'Invalidate additive approvals even when the base policy is unchanged.', + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['settings', 'revokeGrants'], + }, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMarkReadResult', + title: 'AgentMetadataResult', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'observed', + const: 'committed', }, session: { $ref: '#/$defs/AgentSession', @@ -12415,6 +14857,27 @@ export const contracts = { additionalProperties: false, required: ['kind', 'session'], }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'revision_conflict', + }, + expected: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + actual: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'expected', 'actual'], + }, { type: 'object', properties: { @@ -12872,493 +15335,407 @@ export const contracts = { }, resources: {}, }, - mcpConfigurations: { + model: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpConfigurations', + title: 'AgentModelRead', type: 'object', properties: { - workspace: { - anyOf: [ + backend: { + $ref: '#/$defs/AgentBackendPreference', + }, + }, + additionalProperties: false, + required: ['backend'], + $defs: { + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ { - $ref: '#/$defs/AgentWorkspace', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', + }, + connectionId: { + type: 'string', + minLength: 1, + }, + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], }, { - type: 'null', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', + }, + selection: { + $ref: '#/$defs/AgentExecutorSelection', + }, + }, + additionalProperties: false, + required: ['kind', 'selection'], }, ], }, - }, - additionalProperties: false, - $defs: { - AgentWorkspace: { + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { anyOf: [ { - $ref: '#/$defs/Binding', + $ref: '#/$defs/ExecutorEffort', }, { type: 'null', }, ], }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", + runtime: { type: ['string', 'null'], - format: 'uuid', - }, - path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentModelResult', + oneOf: [ + { type: 'object', properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { + kind: { type: 'string', + const: 'ready', }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + choice: { + $ref: '#/$defs/AgentModelChoice', }, - base_commit: { + }, + additionalProperties: false, + required: ['kind', 'choice'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'unavailable', }, - directory: { + message: { type: 'string', }, }, additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpConfigurationList', - type: 'object', - properties: { - locations: { - type: 'array', - items: { - $ref: '#/$defs/AgentMcpConfigurationLocation', - }, + required: ['kind', 'message'], }, - }, - additionalProperties: false, - required: ['locations'], + ], $defs: { - AgentMcpConfigurationLocation: { + AgentModelChoice: { type: 'object', properties: { - location: { - $ref: '#/$defs/ConfigurationLocation', + selection: { + $ref: '#/$defs/AgentBackendPreference', }, - path: { + sourceName: { type: 'string', }, - revision: { + label: { type: 'string', }, - servers: { + thinkingLevels: { type: 'array', items: { - $ref: '#/$defs/AgentMcpConfigurationEntry', + type: 'string', }, }, + defaultThinkingLevel: { + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['location', 'path', 'revision', 'servers'], + required: ['selection', 'sourceName', 'label', 'thinkingLevels'], }, - ConfigurationLocation: { + AgentBackendPreference: { description: - 'Standard user/project configuration locations, ordered from highest precedence.', - type: 'string', - enum: ['project:maka', 'project:agents', 'user:maka', 'user:agents'], - }, - AgentMcpConfigurationEntry: { - type: 'object', - properties: { - name: { - type: 'string', - }, - configuration: true, - shadowedBy: { - anyOf: [ - { - $ref: '#/$defs/ConfigurationLocation', + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', }, - { - type: 'null', + connectionId: { + type: 'string', + minLength: 1, }, - ], - }, - }, - additionalProperties: false, - required: ['name', 'configuration'], - }, - }, - }, - resources: {}, - }, - metadata: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMetadata', - type: 'object', - properties: { - pinned: { - description: "Omission preserves the conversation's position in the catalog.", - type: ['boolean', 'null'], - default: null, - }, - permissions: { - description: 'Omission leaves the permission boundary and its approvals unchanged.', - anyOf: [ - { - $ref: '#/$defs/AgentPermissionChange', + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], }, { - type: 'null', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', + }, + selection: { + $ref: '#/$defs/AgentExecutorSelection', + }, + }, + additionalProperties: false, + required: ['kind', 'selection'], }, ], }, - sessionId: { - type: 'string', - minLength: 1, - }, - expectedRevision: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - title: { + ThinkingLevel: { type: 'string', - minLength: 1, - }, - labels: { - type: 'array', - items: { - type: 'string', - }, + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - }, - additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'title', 'labels'], - $defs: { - AgentPermissionChange: { + AgentExecutorSelection: { type: 'object', properties: { - settings: { - $ref: '#/$defs/AgentWorkspacePermissions', + executor: { + type: 'string', + minLength: 1, }, - revokeGrants: { - description: - 'Invalidate additive approvals even when the base policy is unchanged.', - type: 'boolean', + settings: { + $ref: '#/$defs/ExecutorSettings', }, }, additionalProperties: false, - required: ['settings', 'revokeGrants'], + required: ['executor', 'settings'], }, - AgentWorkspacePermissions: { + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', + model: { + type: ['string', 'null'], }, - requestPermissions: { - type: 'boolean', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['access', 'requestPermissions'], }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], }, }, }, - output: { + resources: {}, + }, + models: { + mode: 'query', + input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMetadataResult', + title: 'AgentModelsRead', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'committed', + const: 'start', }, - session: { - $ref: '#/$defs/AgentSession', + query: { + type: 'string', + default: '', }, }, additionalProperties: false, - required: ['kind', 'session'], + required: ['kind'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'revision_conflict', - }, - expected: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - actual: { - type: 'integer', - format: 'uint64', - minimum: 0, + const: 'continue', }, - }, - additionalProperties: false, - required: ['kind', 'expected', 'actual'], - }, - { - type: 'object', - properties: { - kind: { + revision: { type: 'string', - const: 'rejected', }, - reason: { - $ref: '#/$defs/AgentRejection', + cursor: { + $ref: '#/$defs/AgentModelsCursor', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['kind', 'revision', 'cursor'], }, ], $defs: { - AgentSession: { + AgentModelsCursor: { type: 'object', properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { - type: 'string', - }, - title: { + query: { type: 'string', }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { + offset: { type: 'integer', format: 'uint64', minimum: 0, - }, - archived: { - type: 'boolean', - }, - pinned: { - type: 'boolean', - }, - hasUnread: { - type: 'boolean', - }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], - }, - activity: { - $ref: '#/$defs/AgentSessionActivity', + maximum: 9007199254740991, }, }, additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], + required: ['query', 'offset'], }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentModelsPage', + oneOf: [ + { type: 'object', properties: { - sessionId: { + kind: { type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ - { - description: 'Capture an empty history prefix.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, + const: 'ready', }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', - }, - }, - required: ['kind'], - additionalProperties: false, + revision: { + type: 'string', }, - { - description: 'Include the complete target Turn.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'through_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + choices: { + type: 'array', + items: { + $ref: '#/$defs/AgentModelChoice', + }, }, - { - description: - 'Exclude the complete target Turn, including its submitted inputs.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before_turn', + complete: { + type: 'boolean', + }, + next: { + anyOf: [ + { + $ref: '#/$defs/AgentModelsCursor', }, - turnId: { - type: 'string', - minLength: 1, + { + type: 'null', }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'revision', 'choices', 'complete'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'changed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + ], + $defs: { + AgentModelChoice: { + type: 'object', + properties: { + selection: { + $ref: '#/$defs/AgentBackendPreference', + }, + sourceName: { + type: 'string', + }, + label: { + type: 'string', + }, + thinkingLevels: { + type: 'array', + items: { + type: 'string', }, - additionalProperties: false, - required: ['kind', 'turnId'], }, - ], + defaultThinkingLevel: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['selection', 'sourceName', 'label', 'thinkingLevels'], }, AgentBackendPreference: { description: @@ -13455,331 +15832,313 @@ export const contracts = { description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', - type: 'object', - properties: { - stateDomain: { - type: 'string', - }, - deployment: { - type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, - }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['access', 'requestPermissions'], - }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], - }, - BindingStamp: { + AgentModelsCursor: { type: 'object', properties: { - id: { + query: { type: 'string', - format: 'uuid', }, - permissionEpoch: { + offset: { type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], - }, - AgentWorkspace: { - type: 'object', - properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, - path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', - }, - }, - additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], - }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', - type: 'object', - properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - base_commit: { - type: 'string', - }, - directory: { - type: 'string', - }, - }, - additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], - }, - AgentRejection: { - type: 'string', - enum: [ - 'permission_change_requires_user', - 'title_invalid', - 'labels_invalid', - 'tools_unavailable', - 'busy', - 'workspace_unavailable', - 'session_unavailable', - 'history_unavailable', - 'managed_session', - 'closed', - ], + required: ['query', 'offset'], }, }, }, resources: {}, }, - model: { + observe: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentModelRead', + title: 'AgentSessionRead', type: 'object', properties: { - backend: { - $ref: '#/$defs/AgentBackendPreference', + sessionId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentObservation', + type: 'object', + properties: { + session: { + $ref: '#/$defs/AgentSession', + }, + lastCompletedTurnId: { + description: + "Latest successful source turn at this observation's fence, or none while busy.", + type: ['string', 'null'], + }, + compacting: { + anyOf: [ + { + $ref: '#/$defs/AgentCompactionProgress', + }, + { + type: 'null', + }, + ], + }, + rememberedBackend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - }, - additionalProperties: false, - required: ['backend'], - $defs: { - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ + metrics: { + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], + $ref: '#/$defs/AgentSessionMetrics', }, { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', - }, - }, - additionalProperties: false, - required: ['kind', 'selection'], + type: 'null', }, ], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + through: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - AgentExecutorSelection: { - type: 'object', - properties: { - executor: { - type: 'string', - minLength: 1, + run: { + anyOf: [ + { + $ref: '#/$defs/AgentRun', }, - settings: { - $ref: '#/$defs/ExecutorSettings', + { + type: 'null', }, + ], + }, + parts: { + type: 'array', + items: { + $ref: '#/$defs/AgentPart', }, - additionalProperties: false, - required: ['executor', 'settings'], }, - ExecutorSettings: { + toolSeeds: { description: - 'Executor-owned model selection, independent of Host model connections.', + 'Replay starts for active tool display, read at the same fence as the run.', + type: 'array', + items: { + $ref: '#/$defs/AgentToolSeed', + }, + }, + }, + additionalProperties: false, + required: [ + 'session', + 'lastCompletedTurnId', + 'compacting', + 'through', + 'parts', + 'toolSeeds', + ], + $defs: { + AgentSession: { type: 'object', properties: { - model: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', type: ['string', 'null'], }, - thinkingLevel: { + branch: { anyOf: [ { - $ref: '#/$defs/ExecutorEffort', + $ref: '#/$defs/AgentSessionBranch', }, { type: 'null', }, ], }, - runtime: { - type: ['string', 'null'], + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - }, - additionalProperties: false, - }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentModelResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + dataSource: { + $ref: '#/$defs/AgentDataSource', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + id: { type: 'string', - const: 'ready', }, - choice: { - $ref: '#/$defs/AgentModelChoice', + title: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', }, }, additionalProperties: false, - required: ['kind', 'choice'], + required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', + 'id', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', + ], }, - { + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', type: 'object', properties: { - kind: { + sessionId: { type: 'string', - const: 'unavailable', + minLength: 1, }, - message: { - type: 'string', + position: { + $ref: '#/$defs/AgentForkPosition', }, }, additionalProperties: false, - required: ['kind', 'message'], + required: ['sessionId', 'position'], }, - ], - $defs: { - AgentModelChoice: { - type: 'object', - properties: { - selection: { - $ref: '#/$defs/AgentBackendPreference', - }, - sourceName: { - type: 'string', + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', + oneOf: [ + { + description: 'Capture an empty history prefix.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, }, - label: { - type: 'string', + { + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + }, + required: ['kind'], + additionalProperties: false, }, - thinkingLevels: { - type: 'array', - items: { - type: 'string', + { + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, }, + additionalProperties: false, + required: ['kind', 'turnId'], }, - defaultThinkingLevel: { - type: ['string', 'null'], + { + description: + 'Exclude the complete target Turn, including its submitted inputs.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, - }, - additionalProperties: false, - required: ['selection', 'sourceName', 'label', 'thinkingLevels'], + ], }, AgentBackendPreference: { description: @@ -13876,259 +16235,452 @@ export const contracts = { description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', }, - }, - }, - resources: {}, - }, - models: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentModelsRead', - oneOf: [ - { + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'start', }, - query: { + deployment: { type: 'string', - default: '', + format: 'uuid', + }, + namespace: { + $ref: '#/$defs/InstanceId', }, }, additionalProperties: false, - required: ['kind'], + required: ['stateDomain', 'deployment', 'namespace'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { type: 'object', properties: { - kind: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { type: 'string', - const: 'continue', + format: 'uuid', }, - revision: { + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + AgentWorkspace: { + type: 'object', + properties: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], + }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { type: 'string', + minLength: 1, }, - cursor: { - $ref: '#/$defs/AgentModelsCursor', + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: ['kind', 'revision', 'cursor'], + required: ['path', 'directoryIdentity', 'identity'], }, - ], - $defs: { - AgentModelsCursor: { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { - query: { + id: { type: 'string', }, - offset: { + common_dir: { + type: 'string', + }, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', + }, + }, + additionalProperties: false, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], + }, + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], + }, + AgentCompactionProgress: { + type: 'object', + properties: { + id: { + type: 'string', + }, + startedAt: { + description: 'Original compaction start in milliseconds since the Unix epoch.', type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['query', 'offset'], + required: ['id', 'startedAt'], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentModelsPage', - oneOf: [ - { + AgentSessionMetrics: { + description: 'Display-only readings from the latest completed main-model request.', type: 'object', properties: { - kind: { + modelId: { type: 'string', - const: 'ready', }, - revision: { - type: 'string', + connectionId: { + type: ['string', 'null'], }, - choices: { - type: 'array', - items: { - $ref: '#/$defs/AgentModelChoice', - }, + contextTokens: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, }, - complete: { + contextWindow: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + approximate: { type: 'boolean', }, - next: { - anyOf: [ - { - $ref: '#/$defs/AgentModelsCursor', - }, - { - type: 'null', - }, - ], + contextLastKnown: { + description: + "The context count is the last completed request's baseline; newer additions are unknown.", + type: 'boolean', + }, + outputTokensPerSecond: { + description: + "Provider output tokens divided by the model request's wall time.\nIncludes reasoning and time to first token; excludes tool/user waits.", + type: ['number', 'null'], + format: 'double', }, }, additionalProperties: false, - required: ['kind', 'revision', 'choices', 'complete'], + required: ['modelId', 'approximate', 'contextLastKnown'], }, - { + AgentRun: { type: 'object', properties: { - kind: { + backend: { + $ref: '#/$defs/AgentRunBackend', + }, + invocationId: { + type: 'string', + }, + turnId: { type: 'string', - const: 'changed', + }, + state: { + $ref: '#/$defs/AgentRunState', }, }, - required: ['kind'], additionalProperties: false, + required: ['backend', 'invocationId', 'turnId', 'state'], }, - ], - $defs: { - AgentModelChoice: { + AgentRunBackend: { + description: + 'Execution kind frozen at admission, independent of the next-turn preference.', + type: 'string', + enum: ['model', 'executor'], + }, + AgentRunState: { + type: 'string', + enum: ['admitted', 'running', 'waiting_for_user'], + }, + AgentPart: { type: 'object', properties: { - selection: { - $ref: '#/$defs/AgentBackendPreference', + startSequence: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - sourceName: { + messageId: { type: 'string', }, - label: { + stepId: { type: 'string', }, - thinkingLevels: { - type: 'array', - items: { - type: 'string', - }, + partId: { + type: 'string', }, - defaultThinkingLevel: { - type: ['string', 'null'], + textKind: { + $ref: '#/$defs/AgentContentKind', }, }, additionalProperties: false, - required: ['selection', 'sourceName', 'label', 'thinkingLevels'], + required: ['startSequence', 'messageId', 'stepId', 'partId', 'textKind'], }, - AgentBackendPreference: { + AgentContentKind: { + type: 'string', + enum: ['text', 'thinking'], + }, + AgentToolSeed: { + type: 'object', + properties: { + toolUseId: { + type: 'string', + }, + startSequence: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['toolUseId', 'startSequence'], + }, + }, + }, + resources: {}, + }, + openBot: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentBotOpen', + description: "Open this installation's Bot Session, creating it on first use.", + type: 'object', + properties: { + workspace: { description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + "Where the Bot Session is created on first use. Absent: the Host's first\nconfigured workspace root, so opening the Bot never needs another conversation.", + anyOf: [ + { + $ref: '#/$defs/AgentWorkspaceSelection', + }, + { + type: 'null', + }, + ], + }, + title: { + type: ['string', 'null'], + minLength: 1, + default: null, + }, + }, + additionalProperties: false, + $defs: { + AgentWorkspaceSelection: { oneOf: [ { + description: 'A Git worktree of the directory at `path`, managed by the Host.', type: 'object', properties: { kind: { type: 'string', - const: 'model', + const: 'isolated_git', }, - connectionId: { + path: { type: 'string', minLength: 1, }, - modelId: { + }, + additionalProperties: false, + required: ['kind', 'path'], + }, + { + description: 'A registered Project at one of its locations.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'project', + }, + project: { + $ref: '#/$defs/AgentProjectRef', + }, + path: { type: 'string', minLength: 1, }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + }, + additionalProperties: false, + required: ['kind', 'project', 'path'], + }, + { + description: + 'The directory at `path`: absolute, `~` standing for the home directory,\nwithin the configured workspace locations.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'directory', + }, + path: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], + required: ['kind', 'path'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'executor', + const: 'session', }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', + sessionId: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'selection'], + required: ['kind', 'sessionId'], }, ], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - AgentExecutorSelection: { + AgentProjectRef: { type: 'object', properties: { - executor: { + id: { type: 'string', - minLength: 1, + format: 'uuid', }, - settings: { - $ref: '#/$defs/ExecutorSettings', + revision: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['executor', 'settings'], + required: ['id', 'revision'], }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentBotOpened', + oneOf: [ + { + description: '`created` is false when the existing Bot Session was returned.', type: 'object', properties: { - model: { - type: ['string', 'null'], + kind: { + type: 'string', + const: 'opened', }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + sessionId: { + type: 'string', }, - runtime: { - type: ['string', 'null'], + created: { + type: 'boolean', }, }, additionalProperties: false, + required: ['kind', 'sessionId', 'created'], }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', - }, - AgentModelsCursor: { + { type: 'object', properties: { - query: { + kind: { type: 'string', + const: 'rejected', }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + reason: { + $ref: '#/$defs/AgentBotRejection', }, }, additionalProperties: false, - required: ['query', 'offset'], + required: ['kind', 'reason'], + }, + ], + $defs: { + AgentBotRejection: { + oneOf: [ + { + description: 'The Bot mode is not enabled for this Agent.', + type: 'string', + const: 'disabled', + }, + { + description: 'The Session could not be created; see the creation reason.', + type: 'string', + const: 'creation_rejected', + }, + { + description: + 'No workspace was given and the Host has no configured workspace root.', + type: 'string', + const: 'no_workspace', + }, + ], }, }, }, resources: {}, }, - observe: { + pendingInteractions: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', @@ -14145,737 +16697,660 @@ export const contracts = { }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentObservation', + title: 'AgentPendingInteractions', type: 'object', properties: { - session: { - $ref: '#/$defs/AgentSession', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - lastCompletedTurnId: { - description: - "Latest successful source turn at this observation's fence, or none while busy.", - type: ['string', 'null'], + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentInteractionSummary', + }, }, - compacting: { - anyOf: [ - { - $ref: '#/$defs/AgentCompactionProgress', - }, - { - type: 'null', + }, + additionalProperties: false, + required: ['revision', 'entries'], + $defs: { + AgentInteractionSummary: { + type: 'object', + properties: { + requestId: { + type: 'string', }, - ], - }, - rememberedBackend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', + kind: { + $ref: '#/$defs/AgentInteractionKind', }, - { - type: 'null', + toolUseId: { + type: ['string', 'null'], }, - ], + }, + additionalProperties: false, + required: ['requestId', 'kind', 'toolUseId'], }, - metrics: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionMetrics', - }, - { - type: 'null', - }, + AgentInteractionKind: { + type: 'string', + enum: [ + 'session_approval', + 'service_approval', + 'question', + 'permissions', + 'form', + 'client_capability', + 'computer_app', + 'system_permission', ], }, - through: { + }, + }, + resources: {}, + }, + predictNextMessage: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentPredictNextMessage', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + expectedTurnId: { + type: 'string', + }, + trigger: { + $ref: '#/$defs/AgentPredictionTrigger', + }, + }, + additionalProperties: false, + required: ['sessionId', 'expectedTurnId', 'trigger'], + $defs: { + AgentPredictionTrigger: { + type: 'string', + enum: ['live', 'catch_up'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMessagePrediction', + type: 'object', + properties: { + sourceTurnId: { + type: 'string', + }, + policyRevision: { type: 'integer', format: 'uint64', minimum: 0, }, - run: { - anyOf: [ - { - $ref: '#/$defs/AgentRun', - }, - { - type: 'null', - }, - ], + suggestion: { + type: ['string', 'null'], }, - parts: { - type: 'array', - items: { - $ref: '#/$defs/AgentPart', - }, + }, + additionalProperties: false, + required: ['sourceTurnId', 'policyRevision'], + }, + resources: {}, + }, + previewHistoryCheckpoint: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, }, - toolSeeds: { - description: - 'Replay starts for active tool display, read at the same fence as the run.', + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryCheckpointInventory', + type: 'object', + properties: { + rootSessionId: { + type: 'string', + }, + subtreeDigest: { + type: 'string', + }, + sessions: { type: 'array', items: { - $ref: '#/$defs/AgentToolSeed', + $ref: '#/$defs/AgentHistoryCheckpointSession', }, }, }, additionalProperties: false, - required: [ - 'session', - 'lastCompletedTurnId', - 'compacting', - 'through', - 'parts', - 'toolSeeds', - ], + required: ['rootSessionId', 'subtreeDigest', 'sessions'], $defs: { - AgentSession: { + AgentHistoryCheckpointSession: { type: 'object', properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', + id: { + type: 'string', }, - boundaryRevision: { + revision: { type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { + }, + additionalProperties: false, + required: ['id', 'revision'], + }, + }, + }, + resources: {}, + }, + project: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentProjectChange', + oneOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'register', }, - title: { + path: { type: 'string', + minLength: 1, }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, + expected: { + anyOf: [ + { + $ref: '#/$defs/AgentProjectRef', + }, + { + type: 'null', + }, + ], }, - archived: { - type: 'boolean', + }, + additionalProperties: false, + required: ['kind', 'path', 'expected'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rename', }, - pinned: { - type: 'boolean', + project: { + $ref: '#/$defs/AgentProjectRef', }, - hasUnread: { - type: 'boolean', + name: { + type: 'string', + minLength: 1, }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], + }, + additionalProperties: false, + required: ['kind', 'project', 'name'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'archive', }, - activity: { - $ref: '#/$defs/AgentSessionActivity', + project: { + $ref: '#/$defs/AgentProjectRef', }, }, additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], + required: ['kind', 'project'], }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', + { type: 'object', properties: { - sessionId: { + kind: { type: 'string', - minLength: 1, + const: 'restore', }, - position: { - $ref: '#/$defs/AgentForkPosition', + project: { + $ref: '#/$defs/AgentProjectRef', }, }, additionalProperties: false, - required: ['sessionId', 'position'], + required: ['kind', 'project'], }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ - { - description: 'Capture an empty history prefix.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'relocate', }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', - }, - }, - required: ['kind'], - additionalProperties: false, + project: { + $ref: '#/$defs/AgentProjectRef', }, - { - description: 'Include the complete target Turn.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'through_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + path: { + type: 'string', + minLength: 1, }, - { - description: - 'Exclude the complete target Turn, including its submitted inputs.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before_turn', + destination: { + anyOf: [ + { + $ref: '#/$defs/AgentProjectRef', }, - turnId: { - type: 'string', - minLength: 1, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + ], }, - ], + }, + additionalProperties: false, + required: ['kind', 'project', 'path', 'destination'], }, - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], + ], + $defs: { + AgentProjectRef: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', - }, - }, - additionalProperties: false, - required: ['kind', 'selection'], + revision: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, - ], + }, + additionalProperties: false, + required: ['id', 'revision'], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentProjectResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'committed', + }, + project: { + $ref: '#/$defs/AgentProject', + }, + }, + additionalProperties: false, + required: ['kind', 'project'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'changed', + }, + }, + required: ['kind'], + additionalProperties: false, }, - AgentExecutorSelection: { + { type: 'object', properties: { - executor: { + kind: { type: 'string', - minLength: 1, - }, - settings: { - $ref: '#/$defs/ExecutorSettings', + const: 'unavailable', }, }, + required: ['kind'], additionalProperties: false, - required: ['executor', 'settings'], }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', + { type: 'object', properties: { - model: { - type: ['string', 'null'], - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], - }, - runtime: { - type: ['string', 'null'], + kind: { + type: 'string', + const: 'busy', }, }, + required: ['kind'], additionalProperties: false, }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', - }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', + { type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + kind: { type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', + const: 'invalid', }, }, + required: ['kind'], additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, }, - AgentWorkspacePermissions: { + ], + $defs: { + AgentProject: { type: 'object', properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', + reference: { + $ref: '#/$defs/AgentProjectRef', }, - requestPermissions: { + name: { + type: 'string', + minLength: 1, + }, + archived: { + type: 'boolean', + }, + available: { type: 'boolean', }, + path: { + description: 'The location the Project was last used at, when it is available.', + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['access', 'requestPermissions'], - }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], + required: ['reference', 'name', 'archived', 'available', 'path'], }, - BindingStamp: { + AgentProjectRef: { type: 'object', properties: { id: { type: 'string', format: 'uuid', }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, + revision: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['id', 'revision'], }, - AgentWorkspace: { + }, + }, + resources: {}, + }, + projectMcp: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMcpRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMcpView', + oneOf: [ + { type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], + kind: { + type: 'string', + const: 'absent', }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'configuration', }, path: { type: 'string', - minLength: 1, }, - directoryIdentity: { + revision: { type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + trusted: { + type: 'boolean', + }, + servers: { + type: 'array', + items: { + $ref: '#/$defs/AgentMcpServerView', + }, + }, + disabled: { + type: 'array', + items: { + type: 'string', + }, }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: ['kind', 'path', 'revision', 'trusted', 'servers', 'disabled'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + ], + $defs: { + AgentMcpServerView: { + description: + 'Displayed only to the user. Credentials are omitted, but their values remain\npart of the revision that is explicitly confirmed.', type: 'object', properties: { - id: { + name: { type: 'string', }, - common_dir: { + launch: { type: 'string', }, - source: { - type: 'string', + environment: { + type: 'array', + items: { + type: 'string', + }, }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + headers: { + type: 'array', + items: { + type: 'string', + }, }, - base_commit: { - type: 'string', + preapproved: { + type: 'boolean', }, - directory: { - type: 'string', + readOnlyTools: { + type: 'array', + items: { + type: 'string', + }, }, }, additionalProperties: false, required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', + 'name', + 'launch', + 'environment', + 'headers', + 'preapproved', + 'readOnlyTools', ], }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], - }, - AgentCompactionProgress: { + }, + }, + resources: {}, + }, + projects: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentProjectsRead', + oneOf: [ + { type: 'object', properties: { - id: { + kind: { type: 'string', + const: 'list', }, - startedAt: { - description: 'Original compaction start in milliseconds since the Unix epoch.', - type: 'integer', - format: 'uint64', - minimum: 0, + cursor: { + anyOf: [ + { + $ref: '#/$defs/AgentProjectCursor', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['id', 'startedAt'], + required: ['kind', 'cursor'], }, - AgentSessionMetrics: { - description: 'Display-only readings from the latest completed main-model request.', + { type: 'object', properties: { - modelId: { + kind: { type: 'string', + const: 'directory', }, - connectionId: { - type: ['string', 'null'], - }, - contextTokens: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - contextWindow: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - approximate: { - type: 'boolean', - }, - contextLastKnown: { - description: - "The context count is the last completed request's baseline; newer additions are unknown.", - type: 'boolean', - }, - outputTokensPerSecond: { - description: - "Provider output tokens divided by the model request's wall time.\nIncludes reasoning and time to first token; excludes tool/user waits.", - type: ['number', 'null'], - format: 'double', + path: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['modelId', 'approximate', 'contextLastKnown'], + required: ['kind', 'path'], }, - AgentRun: { + ], + $defs: { + AgentProjectCursor: { type: 'object', properties: { - backend: { - $ref: '#/$defs/AgentRunBackend', - }, - invocationId: { + revision: { type: 'string', + pattern: '^[a-f0-9]{64}$', }, - turnId: { + after: { type: 'string', - }, - state: { - $ref: '#/$defs/AgentRunState', + format: 'uuid', }, }, additionalProperties: false, - required: ['backend', 'invocationId', 'turnId', 'state'], - }, - AgentRunBackend: { - description: - 'Execution kind frozen at admission, independent of the next-turn preference.', - type: 'string', - enum: ['model', 'executor'], - }, - AgentRunState: { - type: 'string', - enum: ['admitted', 'running', 'waiting_for_user'], + required: ['revision', 'after'], }, - AgentPart: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentProjectsPage', + oneOf: [ + { type: 'object', properties: { - startSequence: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - messageId: { + kind: { type: 'string', + const: 'projects', }, - stepId: { + revision: { type: 'string', + pattern: '^[a-f0-9]{64}$', }, - partId: { - type: 'string', + projects: { + type: 'array', + items: { + $ref: '#/$defs/AgentProject', + }, + maxItems: 16, }, - textKind: { - $ref: '#/$defs/AgentContentKind', + next: { + anyOf: [ + { + $ref: '#/$defs/AgentProjectCursor', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['startSequence', 'messageId', 'stepId', 'partId', 'textKind'], - }, - AgentContentKind: { - type: 'string', - enum: ['text', 'thinking'], + required: ['kind', 'revision', 'projects', 'next'], }, - AgentToolSeed: { + { type: 'object', properties: { - toolUseId: { + kind: { type: 'string', + const: 'directory', }, - startSequence: { - type: 'integer', - format: 'uint64', - minimum: 0, + location: { + $ref: '#/$defs/AgentProjectLocation', }, }, additionalProperties: false, - required: ['toolUseId', 'startSequence'], + required: ['kind', 'location'], }, - }, - }, - resources: {}, - }, - openBot: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentBotOpen', - description: "Open this installation's Bot Session, creating it on first use.", - type: 'object', - properties: { - workspace: { - description: - "Where the Bot Session is created on first use. Absent: the Host's first\nconfigured workspace root, so opening the Bot never needs another conversation.", - anyOf: [ - { - $ref: '#/$defs/AgentWorkspaceSelection', - }, - { - type: 'null', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'changed', }, - ], + }, + required: ['kind'], + additionalProperties: false, }, - title: { - type: ['string', 'null'], - minLength: 1, - default: null, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, + }, + required: ['kind'], + additionalProperties: false, }, - }, - additionalProperties: false, + ], $defs: { - AgentWorkspaceSelection: { - oneOf: [ - { - description: 'A Git worktree of the directory at `path`, managed by the Host.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'isolated_git', - }, - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'path'], + AgentProject: { + type: 'object', + properties: { + reference: { + $ref: '#/$defs/AgentProjectRef', }, - { - description: 'A registered Project at one of its locations.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'project', - }, - project: { - $ref: '#/$defs/AgentProjectRef', - }, - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'project', 'path'], + name: { + type: 'string', + minLength: 1, }, - { - description: - 'The directory at `path`: absolute, `~` standing for the home directory,\nwithin the configured workspace locations.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'directory', - }, - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'path'], + archived: { + type: 'boolean', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'session', - }, - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], + available: { + type: 'boolean', + }, + path: { + description: 'The location the Project was last used at, when it is available.', + type: ['string', 'null'], }, - ], + }, + additionalProperties: false, + required: ['reference', 'name', 'archived', 'available', 'path'], }, AgentProjectRef: { type: 'object', @@ -14892,229 +17367,190 @@ export const contracts = { additionalProperties: false, required: ['id', 'revision'], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentBotOpened', - oneOf: [ - { - description: '`created` is false when the existing Bot Session was returned.', + AgentProjectCursor: { type: 'object', properties: { - kind: { + revision: { type: 'string', - const: 'opened', + pattern: '^[a-f0-9]{64}$', }, - sessionId: { + after: { type: 'string', - }, - created: { - type: 'boolean', + format: 'uuid', }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'created'], + required: ['revision', 'after'], }, - { + AgentProjectLocation: { type: 'object', properties: { - kind: { - type: 'string', - const: 'rejected', - }, - reason: { - $ref: '#/$defs/AgentBotRejection', - }, - }, - additionalProperties: false, - required: ['kind', 'reason'], - }, - ], - $defs: { - AgentBotRejection: { - oneOf: [ - { - description: 'The Bot mode is not enabled for this Agent.', - type: 'string', - const: 'disabled', - }, - { - description: 'The Session could not be created; see the creation reason.', - type: 'string', - const: 'creation_rejected', - }, - { - description: - 'No workspace was given and the Host has no configured workspace root.', + path: { type: 'string', - const: 'no_workspace', + minLength: 1, }, - ], - }, - }, - }, - resources: {}, - }, - pendingInteractions: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentPendingInteractions', - type: 'object', - properties: { - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentInteractionSummary', - }, - }, - }, - additionalProperties: false, - required: ['revision', 'entries'], - $defs: { - AgentInteractionSummary: { - type: 'object', - properties: { - requestId: { + name: { type: 'string', + minLength: 1, }, - kind: { - $ref: '#/$defs/AgentInteractionKind', - }, - toolUseId: { - type: ['string', 'null'], + project: { + anyOf: [ + { + $ref: '#/$defs/AgentProject', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['requestId', 'kind', 'toolUseId'], - }, - AgentInteractionKind: { - type: 'string', - enum: [ - 'session_approval', - 'service_approval', - 'question', - 'permissions', - 'form', - 'client_capability', - 'computer_app', - 'system_permission', - ], + required: ['path', 'name', 'project'], }, }, }, resources: {}, }, - predictNextMessage: { + publishHistoryCheckpoint: { mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentPredictNextMessage', + title: 'AgentHistoryCheckpointPublish', type: 'object', properties: { sessionId: { type: 'string', + minLength: 1, }, - expectedTurnId: { - type: 'string', - }, - trigger: { - $ref: '#/$defs/AgentPredictionTrigger', + expectedRevision: { + type: ['string', 'null'], + format: 'uuid', }, - }, - additionalProperties: false, - required: ['sessionId', 'expectedTurnId', 'trigger'], - $defs: { - AgentPredictionTrigger: { + confirmedSubtreeDigest: { + description: + 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', type: 'string', - enum: ['live', 'catch_up'], + pattern: '^[a-f0-9]{64}$', }, }, + additionalProperties: false, + required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMessagePrediction', + title: 'AgentHistoryCheckpointPublication', type: 'object', properties: { - sourceTurnId: { - type: 'string', - }, - policyRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - suggestion: { - type: ['string', 'null'], + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', }, - }, - additionalProperties: false, - required: ['sourceTurnId', 'policyRevision'], - }, - resources: {}, - }, - previewHistoryCheckpoint: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', }, }, additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointInventory', - type: 'object', - properties: { - rootSessionId: { - type: 'string', - }, - subtreeDigest: { - type: 'string', + required: ['state', 'cleanupPending'], + $defs: { + AgentHistoryCheckpointState: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capturing', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'prepared', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'published', + }, + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + }, + additionalProperties: false, + required: ['kind', 'head'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'conflict', + }, + actual: { + type: ['string', 'null'], + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'interrupted', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + ], }, - sessions: { - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryCheckpointSession', + AgentHistoryCheckpointHead: { + type: 'object', + properties: { + revision: { + type: 'string', + format: 'uuid', + }, + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', + }, }, + additionalProperties: false, + required: ['revision', 'manifest'], }, - }, - additionalProperties: false, - required: ['rootSessionId', 'subtreeDigest', 'sessions'], - $defs: { - AgentHistoryCheckpointSession: { + AgentHistoryCheckpointObject: { type: 'object', properties: { - id: { + digest: { type: 'string', + pattern: '^[a-f0-9]{64}$', }, - revision: { + bytes: { type: 'integer', format: 'uint64', minimum: 0, @@ -15122,252 +17558,250 @@ export const contracts = { }, }, additionalProperties: false, - required: ['id', 'revision'], + required: ['digest', 'bytes'], }, }, }, resources: {}, }, - project: { + publishSessionCheckpoint: { mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentProjectChange', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'register', - }, - path: { - type: 'string', - minLength: 1, - }, - expected: { - anyOf: [ - { - $ref: '#/$defs/AgentProjectRef', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'path', 'expected'], + title: 'AgentSessionCheckpointPublish', + type: 'object', + properties: { + history: { + $ref: '#/$defs/AgentHistoryCheckpointPublish', }, - { + workspaceConsistency: { + description: 'Must be explicitly selected. Maka does not fence other applications.', + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + }, + additionalProperties: false, + required: ['history', 'workspaceConsistency'], + $defs: { + AgentHistoryCheckpointPublish: { type: 'object', properties: { - kind: { + sessionId: { type: 'string', - const: 'rename', + minLength: 1, }, - project: { - $ref: '#/$defs/AgentProjectRef', + expectedRevision: { + type: ['string', 'null'], + format: 'uuid', }, - name: { + confirmedSubtreeDigest: { + description: + 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', type: 'string', - minLength: 1, + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['kind', 'project', 'name'], + required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], }, - { - type: 'object', - properties: { - kind: { + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', type: 'string', - const: 'archive', + const: 'caller_confirmed_quiescent', }, - project: { - $ref: '#/$defs/AgentProjectRef', + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', }, - }, - additionalProperties: false, - required: ['kind', 'project'], + ], }, - { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointPublication', + type: 'object', + properties: { + publication: { + $ref: '#/$defs/AgentHistoryCheckpointPublication', + }, + workspaceConsistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + description: 'Stable warning, present even with a caller attestation.', + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + }, + additionalProperties: false, + required: ['publication', 'workspaceConsistency', 'warning'], + $defs: { + AgentHistoryCheckpointPublication: { type: 'object', properties: { - kind: { - type: 'string', - const: 'restore', + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', }, - project: { - $ref: '#/$defs/AgentProjectRef', + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', }, }, additionalProperties: false, - required: ['kind', 'project'], + required: ['state', 'cleanupPending'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'relocate', - }, - project: { - $ref: '#/$defs/AgentProjectRef', + AgentHistoryCheckpointState: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capturing', + }, + }, + required: ['kind'], + additionalProperties: false, }, - path: { - type: 'string', - minLength: 1, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'prepared', + }, + }, + required: ['kind'], + additionalProperties: false, }, - destination: { - anyOf: [ - { - $ref: '#/$defs/AgentProjectRef', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'published', }, - { - type: 'null', + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'project', 'path', 'destination'], - }, - ], - $defs: { - AgentProjectRef: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', + }, + additionalProperties: false, + required: ['kind', 'head'], }, - revision: { - type: 'string', - pattern: '^[a-f0-9]{64}$', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'conflict', + }, + actual: { + type: ['string', 'null'], + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['kind'], }, - }, - additionalProperties: false, - required: ['id', 'revision'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentProjectResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'committed', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'interrupted', + }, + }, + required: ['kind'], + additionalProperties: false, }, - project: { - $ref: '#/$defs/AgentProject', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, + }, + required: ['kind'], + additionalProperties: false, }, - }, - additionalProperties: false, - required: ['kind', 'project'], + ], }, - { + AgentHistoryCheckpointHead: { type: 'object', properties: { - kind: { + revision: { type: 'string', - const: 'changed', + format: 'uuid', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unavailable', + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, - required: ['kind'], additionalProperties: false, + required: ['revision', 'manifest'], }, - { + AgentHistoryCheckpointObject: { type: 'object', properties: { - kind: { + digest: { type: 'string', - const: 'busy', + pattern: '^[a-f0-9]{64}$', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'invalid', + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, - required: ['kind'], additionalProperties: false, + required: ['digest', 'bytes'], }, - ], - $defs: { - AgentProject: { - type: 'object', - properties: { - reference: { - $ref: '#/$defs/AgentProjectRef', - }, - name: { + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', type: 'string', - minLength: 1, - }, - archived: { - type: 'boolean', + const: 'caller_confirmed_quiescent', }, - available: { - type: 'boolean', - }, - path: { - description: 'The location the Project was last used at, when it is available.', - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['reference', 'name', 'archived', 'available', 'path'], - }, - AgentProjectRef: { - type: 'object', - properties: { - id: { + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', type: 'string', - format: 'uuid', + const: 'unverified', }, - revision: { + ], + }, + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', type: 'string', - pattern: '^[a-f0-9]{64}$', + const: 'external_writers_not_fenced', }, - }, - additionalProperties: false, - required: ['id', 'revision'], + ], }, }, }, resources: {}, }, - projectMcp: { + queue: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpRead', + title: 'AgentSessionRead', type: 'object', properties: { sessionId: { type: 'string', + minLength: 1, }, }, additionalProperties: false, @@ -15375,367 +17809,398 @@ export const contracts = { }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMcpView', - oneOf: [ - { + title: 'AgentQueue', + type: 'object', + properties: { + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentQueueEntry', + }, + }, + }, + additionalProperties: false, + required: ['revision', 'entries'], + $defs: { + AgentQueueEntry: { type: 'object', properties: { + origin: { + anyOf: [ + { + $ref: '#/$defs/MessageOrigin', + }, + { + type: 'null', + }, + ], + }, + messageId: { + type: 'string', + }, kind: { + $ref: '#/$defs/AgentQueueKind', + }, + preview: { type: 'string', - const: 'absent', + maxLength: 80, }, }, - required: ['kind'], additionalProperties: false, + required: ['messageId', 'kind', 'preview'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'configuration', + MessageOrigin: { + description: + 'Trusted producer attribution, preserved independently of editable content.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session', + }, + invocation: { + $ref: '#/$defs/Invocation', + }, + }, + additionalProperties: false, + required: ['kind', 'invocation'], }, - path: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'goal', + }, + goalId: { + type: 'string', + }, + objective: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'goalId', 'objective'], }, - revision: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workhub_task', + }, + taskId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'taskId'], }, - trusted: { - type: 'boolean', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workhub_graph', + }, + graphId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'graphId'], }, - servers: { - type: 'array', - items: { - $ref: '#/$defs/AgentMcpServerView', + { + description: 'A worker conversation created by the Bot finished a run.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'bot_worker', + }, + sessionId: { + type: 'string', + }, }, + additionalProperties: false, + required: ['kind', 'sessionId'], }, - disabled: { - type: 'array', - items: { - type: 'string', + { + description: + "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", + type: 'object', + properties: { + kind: { + type: 'string', + const: 'plan', + }, + executionId: { + type: 'string', + }, + title: { + type: 'string', + }, }, + additionalProperties: false, + required: ['kind', 'executionId', 'title'], }, - }, - additionalProperties: false, - required: ['kind', 'path', 'revision', 'trusted', 'servers', 'disabled'], + ], }, - ], - $defs: { - AgentMcpServerView: { - description: - 'Displayed only to the user. Credentials are omitted, but their values remain\npart of the revision that is explicitly confirmed.', + Invocation: { type: 'object', properties: { - name: { + session_id: { type: 'string', }, - launch: { + turn_id: { type: 'string', }, - environment: { - type: 'array', - items: { - type: 'string', - }, - }, - headers: { - type: 'array', - items: { - type: 'string', - }, - }, - preapproved: { - type: 'boolean', + run_id: { + type: 'string', }, - readOnlyTools: { - type: 'array', - items: { - type: 'string', - }, + invocation_id: { + type: 'string', }, }, - additionalProperties: false, - required: [ - 'name', - 'launch', - 'environment', - 'headers', - 'preapproved', - 'readOnlyTools', - ], + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], + }, + AgentQueueKind: { + type: 'string', + enum: ['in_flight', 'steering', 'followup'], }, }, }, resources: {}, }, - projects: { + queueMessage: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentProjectsRead', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'list', - }, - cursor: { - anyOf: [ - { - $ref: '#/$defs/AgentProjectCursor', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'cursor'], + title: 'AgentQueueMessage', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'directory', - }, - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'path'], + messageId: { + type: 'string', + minLength: 1, }, - ], - $defs: { - AgentProjectCursor: { - type: 'object', - properties: { - revision: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - after: { - type: 'string', - format: 'uuid', - }, - }, - additionalProperties: false, - required: ['revision', 'after'], + expectedRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, + additionalProperties: false, + required: ['sessionId', 'messageId', 'expectedRevision'], }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentProjectsPage', + title: 'AgentQueueMessageResult', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'projects', + const: 'ready', }, revision: { - type: 'string', - pattern: '^[a-f0-9]{64}$', + type: 'integer', + format: 'uint64', + minimum: 0, }, - projects: { - type: 'array', - items: { - $ref: '#/$defs/AgentProject', - }, - maxItems: 16, + hasAttachments: { + type: 'boolean', }, - next: { - anyOf: [ - { - $ref: '#/$defs/AgentProjectCursor', - }, - { - type: 'null', - }, - ], + message: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'revision', 'projects', 'next'], + required: ['kind', 'revision', 'hasAttachments', 'message'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'directory', + const: 'revision_conflict', }, - location: { - $ref: '#/$defs/AgentProjectLocation', + expected: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - }, - additionalProperties: false, - required: ['kind', 'location'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'changed', + actual: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'expected', 'actual'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'unavailable', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - ], - $defs: { - AgentProject: { - type: 'object', - properties: { - reference: { - $ref: '#/$defs/AgentProjectRef', - }, - name: { - type: 'string', - minLength: 1, - }, - archived: { - type: 'boolean', - }, - available: { - type: 'boolean', - }, - path: { - description: 'The location the Project was last used at, when it is available.', - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['reference', 'name', 'archived', 'available', 'path'], - }, - AgentProjectRef: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', - }, - revision: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - }, - additionalProperties: false, - required: ['id', 'revision'], - }, - AgentProjectCursor: { - type: 'object', - properties: { - revision: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - after: { - type: 'string', - format: 'uuid', - }, - }, - additionalProperties: false, - required: ['revision', 'after'], - }, - AgentProjectLocation: { - type: 'object', - properties: { - path: { - type: 'string', - minLength: 1, - }, - name: { - type: 'string', - minLength: 1, + const: 'missing', }, - project: { - anyOf: [ - { - $ref: '#/$defs/AgentProject', - }, - { - type: 'null', - }, - ], + reason: { + $ref: '#/$defs/AgentQueueMessageMissingReason', }, }, additionalProperties: false, - required: ['path', 'name', 'project'], + required: ['kind', 'reason'], + }, + ], + $defs: { + AgentQueueMessageMissingReason: { + type: 'string', + enum: ['delivered', 'cancelled', 'unknown'], }, }, }, resources: {}, }, - publishHistoryCheckpoint: { - mode: 'mutation', + readFile: { + mode: 'subscription', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointPublish', + title: 'AgentFileRead', + description: + 'One bounded current-file capture. A version is a precondition, never read authority.', type: 'object', properties: { sessionId: { type: 'string', - minLength: 1, }, - expectedRevision: { - type: ['string', 'null'], - format: 'uuid', + path: { + $ref: '#/$defs/WorkspacePath', }, - confirmedSubtreeDigest: { - description: - 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', - type: 'string', - pattern: '^[a-f0-9]{64}$', + expectedVersion: { + type: ['string', 'null'], + minLength: 71, + maxLength: 71, + pattern: '^sha256:[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], + required: ['sessionId', 'path'], + $defs: { + WorkspacePath: { + type: 'object', + properties: { + bytesBase64: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['bytesBase64'], + }, + }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointPublication', - type: 'object', - properties: { - state: { - $ref: '#/$defs/AgentHistoryCheckpointState', + title: 'Nullable_AgentFileReadPart', + anyOf: [ + { + $ref: '#/$defs/AgentFileReadPart', }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['state', 'cleanupPending'], + ], $defs: { - AgentHistoryCheckpointState: { + AgentFileReadPart: { + description: + 'All parts are subscription items; only End admits a complete preview.\nOffsets and byte counts refer to original bytes, not Base64 or UTF-16 lengths.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'capturing', + const: 'begin', + }, + version: { + type: 'string', + }, + digest: { + type: 'string', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + content: { + $ref: '#/$defs/AgentFileContent', + }, + }, + additionalProperties: false, + required: ['kind', 'version', 'digest', 'bytes', 'content'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'chunk', + }, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + data: { + type: 'string', + maxLength: 43692, + }, + }, + additionalProperties: false, + required: ['kind', 'offset', 'data'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + version: { + type: 'string', + }, + digest: { + type: 'string', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'version', 'digest', 'bytes'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'absent', }, }, required: ['kind'], @@ -15746,7 +18211,52 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'prepared', + const: 'changed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'binary', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'bytes'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unsupported', + }, + fileKind: { + $ref: '#/$defs/AgentFileKind', + }, + }, + additionalProperties: false, + required: ['kind', 'fileKind'], + }, + ], + }, + AgentFileContent: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'text', }, }, required: ['kind'], @@ -15757,51 +18267,147 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'published', + const: 'image', + }, + mimeType: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'mimeType'], + }, + ], + }, + AgentFileKind: { + type: 'string', + enum: ['file', 'directory', 'symlink', 'other'], + }, + }, + }, + resources: {}, + }, + readHistoryCheckpoint: { + mode: 'subscription', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryCheckpointRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + revision: { + description: + 'Only the exact currently retained revision is readable; no latest fallback.', + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['sessionId', 'revision'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentHistoryCheckpointFrame', + anyOf: [ + { + $ref: '#/$defs/AgentHistoryCheckpointFrame', + }, + { + type: 'null', + }, + ], + $defs: { + AgentHistoryCheckpointFrame: { + oneOf: [ + { + description: + 'The streamed payload is history; Workspace is a separate manifest component.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'native_session_v1', }, head: { $ref: '#/$defs/AgentHistoryCheckpointHead', }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointInventory', + }, + bundle: { + $ref: '#/$defs/AgentHistoryCheckpointObject', + }, + workspace: { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, }, additionalProperties: false, - required: ['kind', 'head'], + required: ['kind', 'head', 'inventory', 'bundle', 'workspace'], }, { + description: 'The following bytes use the MAKA-SESSION v2 history format.', type: 'object', properties: { kind: { type: 'string', - const: 'conflict', + const: 'native_history_v1', }, - actual: { - type: ['string', 'null'], - format: 'uuid', + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointInventory', + }, + bundle: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['kind'], + required: ['kind', 'head', 'inventory', 'bundle'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'interrupted', + const: 'chunk', + }, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + data: { + type: 'string', + minLength: 1, + maxLength: 43692, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'offset', 'data'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'unavailable', + const: 'end', + }, + digest: { + type: 'string', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'digest', 'bytes'], }, ], }, @@ -15836,213 +18442,101 @@ export const contracts = { additionalProperties: false, required: ['digest', 'bytes'], }, - }, - }, - resources: {}, - }, - queue: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentQueue', - type: 'object', - properties: { - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentQueueEntry', - }, - }, - }, - additionalProperties: false, - required: ['revision', 'entries'], - $defs: { - AgentQueueEntry: { + AgentHistoryCheckpointInventory: { type: 'object', properties: { - origin: { - anyOf: [ - { - $ref: '#/$defs/MessageOrigin', - }, - { - type: 'null', - }, - ], + rootSessionId: { + type: 'string', }, - messageId: { + subtreeDigest: { type: 'string', }, - kind: { - $ref: '#/$defs/AgentQueueKind', + sessions: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryCheckpointSession', + }, }, - preview: { + }, + additionalProperties: false, + required: ['rootSessionId', 'subtreeDigest', 'sessions'], + }, + AgentHistoryCheckpointSession: { + type: 'object', + properties: { + id: { type: 'string', - maxLength: 80, + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['messageId', 'kind', 'preview'], + required: ['id', 'revision'], }, - MessageOrigin: { - description: - 'Trusted producer attribution, preserved independently of editable content.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'session', - }, - invocation: { - $ref: '#/$defs/Invocation', - }, - }, - additionalProperties: false, - required: ['kind', 'invocation'], + AgentCheckpointWorkspaceSnapshot: { + type: 'object', + properties: { + identity: { + type: 'string', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'goal', - }, - goalId: { - type: 'string', - }, - objective: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'goalId', 'objective'], + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'workhub_task', - }, - taskId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'taskId'], + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'workhub_graph', - }, - graphId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'graphId'], + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, + }, + additionalProperties: false, + required: ['identity', 'consistency', 'warning', 'inventory'], + }, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ { - description: 'A worker conversation created by the Bot finished a run.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'bot_worker', - }, - sessionId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', }, { description: - "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", - type: 'object', - properties: { - kind: { - type: 'string', - const: 'plan', - }, - executionId: { - type: 'string', - }, - title: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'executionId', 'title'], + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', }, ], }, - Invocation: { - type: 'object', - properties: { - session_id: { - type: 'string', - }, - turn_id: { - type: 'string', - }, - run_id: { - type: 'string', - }, - invocation_id: { + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', type: 'string', - }, - }, - required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], - }, - AgentQueueKind: { - type: 'string', - enum: ['in_flight', 'steering', 'followup'], + const: 'external_writers_not_fenced', + }, + ], }, }, }, resources: {}, }, - queueMessage: { + readPatch: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentQueueMessage', + title: 'AgentPatchRead', type: 'object', properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - messageId: { - type: 'string', - minLength: 1, + execution: { + $ref: '#/$defs/AgentExecutionRead', }, - expectedRevision: { + offset: { type: 'integer', format: 'uint64', minimum: 0, @@ -16050,1412 +18544,1506 @@ export const contracts = { }, }, additionalProperties: false, - required: ['sessionId', 'messageId', 'expectedRevision'], + required: ['execution', 'offset'], + $defs: { + AgentExecutionRead: { + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + invocationId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId', 'invocationId'], + }, + }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentQueueMessageResult', + title: 'AgentPatchPage', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'ready', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - hasAttachments: { - type: 'boolean', - }, - message: { - type: 'string', + const: 'unavailable', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'revision', 'hasAttachments', 'message'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'revision_conflict', + const: 'ready', }, - expected: { + patch: { + $ref: '#/$defs/AgentWorkspacePatch', + }, + offset: { type: 'integer', format: 'uint64', minimum: 0, }, - actual: { - type: 'integer', + next: { + type: ['integer', 'null'], format: 'uint64', minimum: 0, }, + text: { + type: 'string', + maxLength: 8192, + }, }, additionalProperties: false, - required: ['kind', 'expected', 'actual'], + required: ['kind', 'patch', 'offset', 'next', 'text'], }, - { + ], + $defs: { + AgentWorkspacePatch: { type: 'object', properties: { - kind: { + artifactId: { type: 'string', - const: 'missing', }, - reason: { - $ref: '#/$defs/AgentQueueMessageMissingReason', + sessionId: { + type: 'string', + }, + turnId: { + type: 'string', + }, + baseCommit: { + type: 'string', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'reason'], - }, - ], - $defs: { - AgentQueueMessageMissingReason: { - type: 'string', - enum: ['delivered', 'cancelled', 'unknown'], + required: ['artifactId', 'sessionId', 'turnId', 'baseCommit', 'bytes'], }, }, }, resources: {}, }, - readFile: { + recall: { mode: 'subscription', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentFileRead', - description: - 'One bounded current-file capture. A version is a precondition, never read authority.', + title: 'RecallQuery', type: 'object', properties: { - sessionId: { - type: 'string', + terms: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, }, - path: { - $ref: '#/$defs/WorkspacePath', + limit: { + description: + 'An explicit caller-selected result budget; omission retains all matching results.', + type: ['integer', 'null'], + format: 'uint', + minimum: 1, }, - expectedVersion: { + session_id: { type: ['string', 'null'], - minLength: 71, - maxLength: 71, - pattern: '^sha256:[a-f0-9]{64}$', + minLength: 1, + }, + since: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + until: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['terms'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentRecallPage', + type: 'object', + properties: { + titles: { + type: 'array', + items: { + $ref: '#/$defs/AgentSession', + }, + }, + passages: { + type: 'array', + items: { + $ref: '#/$defs/AgentRecallPassage', + }, + }, + complete: { + type: 'boolean', + }, + gaps: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + required: ['titles', 'passages', 'complete', 'gaps'], + $defs: { + AgentSession: { + type: 'object', + properties: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], + }, + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], + }, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + dataSource: { + $ref: '#/$defs/AgentDataSource', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + id: { + type: 'string', + }, + title: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', + }, + }, + additionalProperties: false, + required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', + 'id', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', + ], }, - }, - additionalProperties: false, - required: ['sessionId', 'path'], - $defs: { - WorkspacePath: { + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', type: 'object', properties: { - bytesBase64: { + sessionId: { type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', }, }, additionalProperties: false, - required: ['bytesBase64'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentFileReadPart', - anyOf: [ - { - $ref: '#/$defs/AgentFileReadPart', - }, - { - type: 'null', + required: ['sessionId', 'position'], }, - ], - $defs: { - AgentFileReadPart: { - description: - 'All parts are subscription items; only End admits a complete preview.\nOffsets and byte counts refer to original bytes, not Base64 or UTF-16 lengths.', + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', oneOf: [ { + description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'begin', - }, - version: { - type: 'string', - }, - digest: { - type: 'string', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - content: { - $ref: '#/$defs/AgentFileContent', - }, - }, - additionalProperties: false, - required: ['kind', 'version', 'digest', 'bytes', 'content'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'chunk', - }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - data: { - type: 'string', - maxLength: 43692, - }, - }, - additionalProperties: false, - required: ['kind', 'offset', 'data'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', - }, - version: { - type: 'string', - }, - digest: { - type: 'string', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'version', 'digest', 'bytes'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'absent', + const: 'start', }, }, required: ['kind'], additionalProperties: false, }, { + description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'changed', + const: 'end', }, }, required: ['kind'], additionalProperties: false, }, { + description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'binary', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'bytes'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unsupported', - }, - fileKind: { - $ref: '#/$defs/AgentFileKind', + const: 'through_turn', }, - }, - additionalProperties: false, - required: ['kind', 'fileKind'], - }, - ], - }, - AgentFileContent: { - oneOf: [ - { - type: 'object', - properties: { - kind: { + turnId: { type: 'string', - const: 'text', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'turnId'], }, { + description: + 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'image', + const: 'before_turn', }, - mimeType: { + turnId: { type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'mimeType'], + required: ['kind', 'turnId'], }, ], }, - AgentFileKind: { - type: 'string', - enum: ['file', 'directory', 'symlink', 'other'], - }, - }, - }, - resources: {}, - }, - readHistoryCheckpoint: { - mode: 'subscription', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - revision: { + AgentBackendPreference: { description: - 'Only the exact currently retained revision is readable; no latest fallback.', - type: 'string', - format: 'uuid', - }, - }, - additionalProperties: false, - required: ['sessionId', 'revision'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentHistoryCheckpointFrame', - anyOf: [ - { - $ref: '#/$defs/AgentHistoryCheckpointFrame', - }, - { - type: 'null', - }, - ], - $defs: { - AgentHistoryCheckpointFrame: { + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", oneOf: [ { - description: 'The following bytes use the MAKA-SESSION v2 history format.', type: 'object', properties: { kind: { type: 'string', - const: 'native_history_v1', - }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', - }, - inventory: { - $ref: '#/$defs/AgentHistoryCheckpointInventory', - }, - bundle: { - $ref: '#/$defs/AgentHistoryCheckpointObject', + const: 'model', }, - }, - additionalProperties: false, - required: ['kind', 'head', 'inventory', 'bundle'], - }, - { - type: 'object', - properties: { - kind: { + connectionId: { type: 'string', - const: 'chunk', - }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + minLength: 1, }, - data: { + modelId: { type: 'string', minLength: 1, - maxLength: 43692, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'offset', 'data'], + required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'end', - }, - digest: { - type: 'string', + const: 'executor', }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'digest', 'bytes'], + required: ['kind', 'selection'], }, ], }, - AgentHistoryCheckpointHead: { + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { type: 'object', properties: { - revision: { + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { type: 'string', format: 'uuid', }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', + namespace: { + $ref: '#/$defs/InstanceId', }, }, additionalProperties: false, - required: ['revision', 'manifest'], + required: ['stateDomain', 'deployment', 'namespace'], }, - AgentHistoryCheckpointObject: { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { type: 'object', properties: { - digest: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { type: 'string', - pattern: '^[a-f0-9]{64}$', + format: 'uuid', }, - bytes: { + permissionEpoch: { type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['digest', 'bytes'], + required: ['id', 'permissionEpoch'], }, - AgentHistoryCheckpointInventory: { + AgentWorkspace: { type: 'object', properties: { - rootSessionId: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], + }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { + type: 'string', + minLength: 1, + }, + directoryIdentity: { type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - subtreeDigest: { + identity: { type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, - sessions: { + }, + additionalProperties: false, + required: ['path', 'directoryIdentity', 'identity'], + }, + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', + type: 'object', + properties: { + id: { + type: 'string', + }, + common_dir: { + type: 'string', + }, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', + }, + }, + additionalProperties: false, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], + }, + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], + }, + AgentRecallPassage: { + type: 'object', + properties: { + session: { + $ref: '#/$defs/AgentSession', + }, + anchorMessageId: { + type: 'string', + }, + anchorPosition: { + $ref: '#/$defs/HistoryRowPosition', + }, + excerpt: { + description: + 'Display preview beginning at the first match. The anchor opens the complete source.', + type: 'string', + }, + highlights: { + description: 'Highlight ranges in UTF-16 code units within `excerpt`.', type: 'array', items: { - $ref: '#/$defs/AgentHistoryCheckpointSession', + $ref: '#/$defs/AgentRecallHighlight', + }, + }, + matchedTerms: { + type: 'array', + items: { + type: 'string', }, }, }, additionalProperties: false, - required: ['rootSessionId', 'subtreeDigest', 'sessions'], + required: [ + 'session', + 'anchorMessageId', + 'anchorPosition', + 'excerpt', + 'highlights', + 'matchedTerms', + ], + }, + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + type: 'object', + properties: { + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sourceSequence', 'itemIndex'], }, - AgentHistoryCheckpointSession: { + AgentRecallHighlight: { type: 'object', properties: { - id: { - type: 'string', + start: { + type: 'integer', + format: 'uint', + minimum: 0, }, - revision: { + end: { type: 'integer', - format: 'uint64', + format: 'uint', minimum: 0, - maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['id', 'revision'], + required: ['start', 'end'], }, }, }, resources: {}, }, - readPatch: { + recap: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentPatchRead', + title: 'AgentRecapRead', type: 'object', properties: { - execution: { - $ref: '#/$defs/AgentExecutionRead', + sessionId: { + type: 'string', }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + operationId: { + type: ['string', 'null'], + format: 'uuid', + default: null, }, }, additionalProperties: false, - required: ['execution', 'offset'], - $defs: { - AgentExecutionRead: { - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - invocationId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId', 'invocationId'], - }, - }, + required: ['sessionId'], }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentPatchPage', - oneOf: [ + title: 'Nullable_AgentRecapReceipt', + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unavailable', - }, - }, - required: ['kind'], - additionalProperties: false, + $ref: '#/$defs/AgentRecapReceipt', }, { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'ready', - }, - patch: { - $ref: '#/$defs/AgentWorkspacePatch', - }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - next: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - text: { - type: 'string', - maxLength: 8192, - }, - }, - additionalProperties: false, - required: ['kind', 'patch', 'offset', 'next', 'text'], + type: 'null', }, ], $defs: { - AgentWorkspacePatch: { - type: 'object', - properties: { - artifactId: { - type: 'string', - }, - sessionId: { - type: 'string', - }, - turnId: { - type: 'string', + AgentRecapReceipt: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'pending', + }, + operationId: { + type: 'string', + format: 'uuid', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'operationId', 'through'], }, - baseCommit: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'ready', + }, + operationId: { + type: 'string', + format: 'uuid', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + text: { + type: 'string', + }, + modelId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'operationId', 'through', 'text', 'modelId'], }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'failed', + }, + operationId: { + type: 'string', + format: 'uuid', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + reason: { + $ref: '#/$defs/AgentRecapFailure', + }, + }, + additionalProperties: false, + required: ['kind', 'operationId', 'through', 'reason'], }, - }, - additionalProperties: false, - required: ['artifactId', 'sessionId', 'turnId', 'baseCommit', 'bytes'], + ], + }, + AgentRecapFailure: { + type: 'string', + enum: ['empty_or_oversized_output', 'incomplete_output', 'model_unavailable'], }, }, }, resources: {}, }, - recall: { - mode: 'subscription', + respond: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'RecallQuery', + title: 'AgentInteractionRespond', type: 'object', properties: { - terms: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - minItems: 1, - }, - limit: { - description: - 'An explicit caller-selected result budget; omission retains all matching results.', - type: ['integer', 'null'], - format: 'uint', - minimum: 1, - }, - session_id: { - type: ['string', 'null'], + sessionId: { + type: 'string', minLength: 1, }, - since: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - until: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['terms'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentRecallPage', - type: 'object', - properties: { - titles: { - type: 'array', - items: { - $ref: '#/$defs/AgentSession', - }, - }, - passages: { - type: 'array', - items: { - $ref: '#/$defs/AgentRecallPassage', - }, - }, - complete: { - type: 'boolean', + requestId: { + type: 'string', + minLength: 1, }, - gaps: { - type: 'array', - items: { - type: 'string', - }, + answer: { + $ref: '#/$defs/InteractionAnswer', }, }, additionalProperties: false, - required: ['titles', 'passages', 'complete', 'gaps'], + required: ['sessionId', 'requestId', 'answer'], $defs: { - AgentSession: { - type: 'object', - properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', + InteractionAnswer: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session_approval', }, - { - type: 'null', + decision: { + $ref: '#/$defs/Decision', }, - ], + }, + additionalProperties: false, + required: ['kind', 'decision'], }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'service_approval', }, - { - type: 'null', + decision: { + $ref: '#/$defs/Decision', }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', }, + additionalProperties: false, + required: ['kind', 'decision'], }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { - type: 'string', - }, - title: { - type: 'string', - }, - labels: { - type: 'array', - items: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'permissions', + }, + decision: { + $ref: '#/$defs/Decision2', + }, }, + additionalProperties: false, + required: ['kind', 'decision'], }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - archived: { - type: 'boolean', - }, - pinned: { - type: 'boolean', - }, - hasUnread: { - type: 'boolean', - }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], - }, - activity: { - $ref: '#/$defs/AgentSessionActivity', - }, - }, - additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], - }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ { - description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'start', + const: 'question', + }, + answers: { + type: 'array', + items: { + type: ['string', 'null'], + }, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'answers'], }, { - description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'end', + const: 'form', }, }, + unevaluatedProperties: false, + oneOf: [ + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'accept', + }, + values: { + type: 'object', + additionalProperties: { + $ref: '#/$defs/FormValue', + }, + }, + }, + required: ['action', 'values'], + }, + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'decline', + }, + }, + required: ['action'], + }, + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'cancel', + }, + }, + required: ['action'], + }, + ], required: ['kind'], - additionalProperties: false, }, { - description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'through_turn', + const: 'client_capability', }, - turnId: { + decision: { + $ref: '#/$defs/Decision', + }, + }, + additionalProperties: false, + required: ['kind', 'decision'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'computer_app', + }, + decision: { + $ref: '#/$defs/ComputerAppDecision', }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind', 'decision'], }, { - description: - 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'before_turn', + const: 'computer_app_cancel', }, - turnId: { + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'system_permission_cancel', }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind'], }, ], }, - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + Decision: { + type: 'string', + enum: ['allow', 'deny'], + }, + Decision2: { oneOf: [ { type: 'object', properties: { - kind: { - type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { + decision: { type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + const: 'deny', }, }, + required: ['decision'], additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { - kind: { + decision: { type: 'string', - const: 'executor', + const: 'allow', }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', + permissions: { + $ref: '#/$defs/Permissions', + }, + scope: { + $ref: '#/$defs/Scope2', }, }, additionalProperties: false, - required: ['kind', 'selection'], + required: ['decision', 'permissions', 'scope'], }, ], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - AgentExecutorSelection: { - type: 'object', - properties: { - executor: { - type: 'string', - minLength: 1, - }, - settings: { - $ref: '#/$defs/ExecutorSettings', - }, - }, - additionalProperties: false, - required: ['executor', 'settings'], - }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', + Permissions: { type: 'object', properties: { - model: { - type: ['string', 'null'], - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + filesystem: { + type: 'array', + items: { + $ref: '#/$defs/Rule', + }, }, - runtime: { - type: ['string', 'null'], + network: { + $ref: '#/$defs/Network', }, }, additionalProperties: false, + required: ['filesystem', 'network'], }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', - }, - AgentDataSource: { + Rule: { description: - 'Stable business-data origin, independent of package or activation revisions.', + 'Paths have executor-native meaning. Validate and materialize them on the\nexecution host, never on a remote Desktop. This document alone grants nothing.', type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + path: { type: 'string', - format: 'uuid', }, - namespace: { - $ref: '#/$defs/InstanceId', + scope: { + $ref: '#/$defs/Scope', }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, - }, - AgentWorkspacePermissions: { - type: 'object', - properties: { access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', + $ref: '#/$defs/Access', }, }, additionalProperties: false, - required: ['access', 'requestPermissions'], + required: ['path', 'scope', 'access'], }, - AgentWorkspaceAccess: { + Scope: { + type: 'string', + enum: ['subtree', 'exact'], + }, + Access: { description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + 'Equal-path conflict priority is different from authority breadth: deny wins.', type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], + enum: ['read', 'write', 'deny'], }, - BindingStamp: { - type: 'object', - properties: { - id: { + Network: { + oneOf: [ + { type: 'string', - format: 'uuid', + enum: ['denied', 'allowed'], }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'object', + properties: { + restricted: { + type: 'object', + properties: { + destinations: { + type: 'array', + uniqueItems: true, + items: { + $ref: '#/$defs/Destination', + }, + }, + }, + additionalProperties: false, + required: ['destinations'], + }, + }, + required: ['restricted'], + additionalProperties: false, }, - }, - additionalProperties: false, - required: ['id', 'permissionEpoch'], + ], }, - AgentWorkspace: { + Destination: { + description: + 'Destination authority, independent of the upstream proxy used to reach it.\nA domain grant does not authorize its subdomains or other ports.', type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, - path: { + host: { type: 'string', minLength: 1, + maxLength: 253, }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + port: { + type: 'integer', + format: 'uint16', + minimum: 1, + maximum: 65535, }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: ['host', 'port'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', - type: 'object', - properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { + Scope2: { + type: 'string', + enum: ['once', 'turn', 'session'], + }, + FormValue: { + anyOf: [ + { type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', }, - base_commit: { - type: 'string', + { + type: 'number', + format: 'double', }, - directory: { - type: 'string', + { + type: 'boolean', + }, + { + type: 'array', + items: { + type: 'string', + }, }, - }, - additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', ], }, - AgentSessionActivity: { + ComputerAppDecision: { type: 'string', - enum: ['idle', 'working', 'waiting'], + enum: ['once', 'session', 'always', 'deny'], }, - AgentRecallPassage: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentInteractionResult', + oneOf: [ + { + description: + 'Includes a concurrent winner. Consumers inspect the canonical outcome.', type: 'object', properties: { - session: { - $ref: '#/$defs/AgentSession', - }, - anchorMessageId: { - type: 'string', - }, - anchorPosition: { - $ref: '#/$defs/HistoryRowPosition', - }, - excerpt: { - description: - 'Display preview beginning at the first match. The anchor opens the complete source.', + kind: { type: 'string', + const: 'recorded', }, - highlights: { - description: 'Highlight ranges in UTF-16 code units within `excerpt`.', - type: 'array', - items: { - $ref: '#/$defs/AgentRecallHighlight', - }, - }, - matchedTerms: { - type: 'array', - items: { - type: 'string', - }, + record: { + $ref: '#/$defs/InteractionRecord', }, }, additionalProperties: false, - required: [ - 'session', - 'anchorMessageId', - 'anchorPosition', - 'excerpt', - 'highlights', - 'matchedTerms', - ], + required: ['kind', 'record'], }, - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + { type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + kind: { + type: 'string', + const: 'rejected', }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + reason: { + $ref: '#/$defs/AgentInteractionRejection', }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['kind', 'reason'], }, - AgentRecallHighlight: { + ], + $defs: { + InteractionRecord: { type: 'object', properties: { - start: { - type: 'integer', - format: 'uint', - minimum: 0, + sessionId: { + type: 'string', }, - end: { + turnId: { + type: 'string', + }, + runId: { + type: 'string', + }, + requestId: { + type: 'string', + }, + createdAt: { type: 'integer', - format: 'uint', + format: 'uint64', minimum: 0, }, + request: { + $ref: '#/$defs/InteractionRequest', + }, + outcome: { + anyOf: [ + { + $ref: '#/$defs/InteractionOutcome', + }, + { + type: 'null', + }, + ], + }, }, additionalProperties: false, - required: ['start', 'end'], - }, - }, - }, - resources: {}, - }, - recap: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentRecapRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - operationId: { - type: ['string', 'null'], - format: 'uuid', - default: null, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentRecapReceipt', - anyOf: [ - { - $ref: '#/$defs/AgentRecapReceipt', - }, - { - type: 'null', + required: [ + 'sessionId', + 'turnId', + 'runId', + 'requestId', + 'createdAt', + 'request', + 'outcome', + ], }, - ], - $defs: { - AgentRecapReceipt: { + InteractionRequest: { oneOf: [ { + description: + 'Human consent for the exact decision proposed for a child interaction.', type: 'object', properties: { kind: { type: 'string', - const: 'pending', + const: 'session_approval', }, - operationId: { + toolUseId: { type: 'string', - format: 'uuid', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + sessionId: { + type: 'string', + }, + requestId: { + type: 'string', + }, + answer: { + $ref: '#/$defs/SessionApprovalAnswer', }, }, additionalProperties: false, - required: ['kind', 'operationId', 'through'], + required: ['kind', 'toolUseId', 'basis', 'sessionId', 'requestId', 'answer'], }, { + description: + 'Consent for one already journaled service call; never a service grant.', type: 'object', properties: { kind: { type: 'string', - const: 'ready', + const: 'service_approval', }, - operationId: { + toolUseId: { type: 'string', - format: 'uuid', - }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, }, - text: { + toolName: { type: 'string', }, - modelId: { + service: { type: 'string', }, - }, - additionalProperties: false, - required: ['kind', 'operationId', 'through', 'text', 'modelId'], - }, - { - type: 'object', - properties: { - kind: { + method: { type: 'string', - const: 'failed', }, - operationId: { + binding: { type: 'string', - format: 'uuid', }, through: { type: 'integer', format: 'uint64', minimum: 0, }, - reason: { - $ref: '#/$defs/AgentRecapFailure', - }, }, additionalProperties: false, - required: ['kind', 'operationId', 'through', 'reason'], - }, - ], - }, - AgentRecapFailure: { - type: 'string', - enum: ['empty_or_oversized_output', 'incomplete_output', 'model_unavailable'], - }, - }, - }, - resources: {}, - }, - respond: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentInteractionRespond', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - requestId: { - type: 'string', - minLength: 1, - }, - answer: { - $ref: '#/$defs/InteractionAnswer', - }, - }, - additionalProperties: false, - required: ['sessionId', 'requestId', 'answer'], - $defs: { - InteractionAnswer: { - oneOf: [ + required: [ + 'kind', + 'toolUseId', + 'toolName', + 'service', + 'method', + 'binding', + 'through', + ], + }, { type: 'object', properties: { kind: { type: 'string', - const: 'session_approval', + const: 'permissions', }, - decision: { - $ref: '#/$defs/Decision', + toolUseId: { + type: ['string', 'null'], + }, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + request: { + $ref: '#/$defs/PermissionRequest', }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'toolUseId', 'basis', 'request'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'service_approval', + const: 'question', }, - decision: { - $ref: '#/$defs/Decision', + toolUseId: { + type: 'string', + }, + questions: { + type: 'array', + items: { + $ref: '#/$defs/InteractionQuestion', + }, }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'toolUseId', 'questions'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'permissions', + const: 'form', }, - decision: { - $ref: '#/$defs/Decision2', + toolUseId: { + type: 'string', + }, + message: { + type: 'string', + }, + requester: { + $ref: '#/$defs/FormRequester', + }, + fields: { + type: 'array', + items: { + $ref: '#/$defs/FormField', + }, }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'toolUseId', 'message', 'requester', 'fields'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'question', + const: 'client_capability', }, - answers: { - type: 'array', - items: { - type: ['string', 'null'], - }, + toolUseId: { + type: 'string', + }, + target: { + $ref: '#/$defs/GrantTarget', + }, + basis: { + description: + 'Agent-owned approvals expire with this original permission boundary.\nUnbound capabilities are explicit; Agent-owned capabilities capture a full binding stamp.', + anyOf: [ + { + $ref: '#/$defs/PermissionBasis', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'answers'], + required: ['kind', 'toolUseId', 'target'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'form', + const: 'computer_app', }, - }, - unevaluatedProperties: false, - oneOf: [ - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'accept', - }, - values: { - type: 'object', - additionalProperties: { - $ref: '#/$defs/FormValue', - }, - }, - }, - required: ['action', 'values'], + toolUseId: { + type: 'string', }, - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'decline', - }, - }, - required: ['action'], + nativeOperationId: { + type: 'string', }, - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'cancel', - }, - }, - required: ['action'], + target: { + $ref: '#/$defs/GrantTarget', + }, + basis: { + $ref: '#/$defs/PermissionBasis', + }, + display: { + $ref: '#/$defs/ComputerAppDisplay', + }, + allowAlways: { + type: 'boolean', + }, + authorityRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, + }, + additionalProperties: false, + required: [ + 'kind', + 'toolUseId', + 'nativeOperationId', + 'target', + 'basis', + 'display', + 'allowAlways', + 'authorityRevision', ], - required: ['kind'], }, { + description: + 'Missing operating-system grants the waiting tool call needs. The producer\npolls the real grant state and commits `SystemPermissionGranted` itself;\nthe user can only cancel.', type: 'object', properties: { kind: { type: 'string', - const: 'client_capability', + const: 'system_permission', }, - decision: { - $ref: '#/$defs/Decision', + toolUseId: { + type: 'string', + }, + grants: { + description: 'Distinct missing grants.', + type: 'array', + items: { + $ref: '#/$defs/SystemPermissionGrant', + }, + minItems: 1, + }, + application: { + description: + 'The application name the operating system lists in its settings.', + type: 'string', + minLength: 1, + }, + applicationLocation: { + anyOf: [ + { + $ref: '#/$defs/SystemPermissionApplicationLocation', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'toolUseId', 'grants', 'application'], + }, + ], + }, + PermissionBasis: { + description: + 'Captured authority, never silently upgraded by resolving a current binding.', + type: 'object', + properties: { + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + }, + additionalProperties: false, + required: ['boundaryRevision', 'binding'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + SessionApprovalAnswer: { + description: + "Only access-expanding child decisions can require a parent's human confirmation.", + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', + const: 'service_approval', }, decision: { - $ref: '#/$defs/ComputerAppDecision', + $ref: '#/$defs/ApprovalAllow', }, }, additionalProperties: false, @@ -17466,42 +20054,51 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'computer_app_cancel', + const: 'permissions', + }, + decision: { + $ref: '#/$defs/PermissionApproval', }, }, additionalProperties: false, - required: ['kind'], + required: ['kind', 'decision'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'system_permission_cancel', + const: 'client_capability', + }, + decision: { + $ref: '#/$defs/ApprovalAllow', }, }, additionalProperties: false, - required: ['kind'], + required: ['kind', 'decision'], }, - ], - }, - Decision: { - type: 'string', - enum: ['allow', 'deny'], - }, - Decision2: { - oneOf: [ { type: 'object', properties: { - decision: { + kind: { type: 'string', - const: 'deny', + const: 'computer_app', + }, + decision: { + $ref: '#/$defs/ComputerAppApproval', }, }, - required: ['decision'], additionalProperties: false, + required: ['kind', 'decision'], }, + ], + }, + ApprovalAllow: { + type: 'string', + enum: ['allow'], + }, + PermissionApproval: { + oneOf: [ { type: 'object', properties: { @@ -17618,1539 +20215,1907 @@ export const contracts = { type: 'string', enum: ['once', 'turn', 'session'], }, - FormValue: { - anyOf: [ - { + ComputerAppApproval: { + type: 'string', + enum: ['once', 'session', 'always'], + }, + PermissionRequest: { + description: + 'The Host supplies the real operation description; it is display information,\nnot authority inferred from a command string or a plugin-supplied Session ID.', + type: 'object', + properties: { + reason: { + type: 'string', + }, + command: { + anyOf: [ + { + $ref: '#/$defs/PermissionCommand', + }, + { + type: 'null', + }, + ], + }, + permissions: { + $ref: '#/$defs/Permissions', + }, + }, + additionalProperties: false, + required: ['reason', 'permissions'], + }, + PermissionCommand: { + type: 'object', + properties: { + command: { + type: 'string', + }, + cwd: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['command', 'cwd'], + }, + InteractionQuestion: { + type: 'object', + properties: { + question: { + type: 'string', + minLength: 1, + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/QuestionOption', + }, + minItems: 2, + maxItems: 3, + }, + }, + additionalProperties: false, + required: ['question', 'options'], + }, + QuestionOption: { + type: 'object', + properties: { + label: { + type: 'string', + minLength: 1, + }, + description: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['label'], + }, + FormRequester: { + type: 'object', + properties: { + name: { + type: 'string', + }, + source: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['name'], + }, + FormField: { + type: 'object', + properties: { + name: { + type: 'string', + }, + label: { type: 'string', }, + required: { + type: 'boolean', + }, + description: { + type: ['string', 'null'], + }, + }, + required: ['name', 'label', 'required'], + oneOf: [ { - type: 'number', - format: 'double', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'string', + }, + default: { + type: ['string', 'null'], + }, + minLength: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + maxLength: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + format: { + anyOf: [ + { + $ref: '#/$defs/FormFormat', + }, + { + type: 'null', + }, + ], + }, + }, + required: ['kind'], }, { - type: 'boolean', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'number', + }, + default: { + type: ['number', 'null'], + format: 'double', + }, + minimum: { + type: ['number', 'null'], + format: 'double', + }, + maximum: { + type: ['number', 'null'], + format: 'double', + }, + }, + required: ['kind'], }, { - type: 'array', - items: { - type: 'string', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'integer', + }, + default: { + type: ['number', 'null'], + format: 'double', + }, + minimum: { + type: ['number', 'null'], + format: 'double', + }, + maximum: { + type: ['number', 'null'], + format: 'double', + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'boolean', + }, + default: { + type: ['boolean', 'null'], + }, + }, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'single_select', + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/FormOption', + }, + }, + default: { + type: ['string', 'null'], + }, + }, + required: ['kind', 'options'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'multi_select', + }, + options: { + type: 'array', + items: { + $ref: '#/$defs/FormOption', + }, + }, + default: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + minItems: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, + maxItems: { + type: ['integer', 'null'], + format: 'uint', + minimum: 0, + }, }, + required: ['kind', 'options'], }, ], }, - ComputerAppDecision: { + FormFormat: { type: 'string', - enum: ['once', 'session', 'always', 'deny'], + enum: ['email', 'uri', 'date', 'date-time'], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentInteractionResult', - oneOf: [ - { - description: - 'Includes a concurrent winner. Consumers inspect the canonical outcome.', + FormOption: { type: 'object', properties: { - kind: { + value: { type: 'string', - const: 'recorded', - }, - record: { - $ref: '#/$defs/InteractionRecord', }, - }, - additionalProperties: false, - required: ['kind', 'record'], - }, - { - type: 'object', - properties: { - kind: { + label: { type: 'string', - const: 'rejected', - }, - reason: { - $ref: '#/$defs/AgentInteractionRejection', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['value', 'label'], }, - ], - $defs: { - InteractionRecord: { + GrantTarget: { type: 'object', properties: { - sessionId: { + providerId: { type: 'string', }, - turnId: { + contractId: { type: 'string', }, - runId: { + serverId: { type: 'string', }, - requestId: { + toolName: { type: 'string', }, - createdAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - request: { - $ref: '#/$defs/InteractionRequest', + capability: { + $ref: '#/$defs/GrantCapability', }, - outcome: { - anyOf: [ - { - $ref: '#/$defs/InteractionOutcome', - }, - { - type: 'null', - }, - ], + scope: { + $ref: '#/$defs/GrantScope', }, }, - additionalProperties: false, required: [ - 'sessionId', - 'turnId', - 'runId', - 'requestId', - 'createdAt', - 'request', - 'outcome', + 'providerId', + 'contractId', + 'serverId', + 'toolName', + 'capability', + 'scope', ], }, - InteractionRequest: { + GrantCapability: { oneOf: [ + { + type: 'string', + enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], + }, { description: - 'Human consent for the exact decision proposed for a child interaction.', + 'File download consent for a browser origin, independent of page access.', + type: 'string', + const: 'browser_download', + }, + { + description: + "Read the browser profile's browsing history, independent of page access.", + type: 'string', + const: 'browser_history', + }, + { + description: + 'File upload consent for a browser origin, independent of reads and downloads.', + type: 'string', + const: 'browser_upload', + }, + ], + }, + GrantScope: { + oneOf: [ + { type: 'object', properties: { kind: { type: 'string', - const: 'session_approval', + const: 'browser_origin', }, - toolUseId: { + origin: { type: 'string', }, - basis: { - $ref: '#/$defs/PermissionBasis', + }, + additionalProperties: false, + required: ['kind', 'origin'], + }, + { + description: + 'One selected browser profile generation; never an origin or app-wide grant.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'browser_profile', }, - sessionId: { + browserId: { type: 'string', }, - requestId: { + generation: { type: 'string', }, - answer: { - $ref: '#/$defs/SessionApprovalAnswer', + }, + additionalProperties: false, + required: ['kind', 'browserId', 'generation'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capability', }, }, additionalProperties: false, - required: ['kind', 'toolUseId', 'basis', 'sessionId', 'requestId', 'answer'], + required: ['kind'], }, { - description: - 'Consent for one already journaled service call; never a service grant.', type: 'object', properties: { kind: { type: 'string', - const: 'service_approval', + const: 'mcp_tool', }, - toolUseId: { + serverId: { type: 'string', }, toolName: { type: 'string', }, - service: { + }, + additionalProperties: false, + required: ['kind', 'serverId', 'toolName'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'computer_app', }, - method: { + platform: { + $ref: '#/$defs/ComputerAppPlatform', + }, + applicationId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'platform', 'applicationId'], + }, + ], + }, + ComputerAppPlatform: { + type: 'string', + enum: ['macos', 'windows'], + }, + ComputerAppDisplay: { + type: 'object', + properties: { + name: { + type: 'string', + }, + appPath: { + type: 'string', + }, + risk: { + anyOf: [ + { + $ref: '#/$defs/ComputerAppRisk', + }, + { + type: 'null', + }, + ], + }, + warning: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['name', 'appPath'], + }, + ComputerAppRisk: { + type: 'string', + enum: ['low', 'high'], + }, + SystemPermissionGrant: { + description: + 'A macOS privacy grant (TCC) that the Host process itself needs before native\nComputer Use can run. This is operating-system state the user changes in\nSystem Settings, not a Maka grant; the Host observes it and commits the outcome.', + oneOf: [ + { + description: 'System Settings > Privacy & Security > Accessibility.', + type: 'string', + const: 'accessibility', + }, + { + description: + 'System Settings > Privacy & Security > Screen & System Audio Recording.', + type: 'string', + const: 'screen_recording', + }, + ], + }, + SystemPermissionApplicationLocation: { + type: 'object', + properties: { + fileUrl: { + type: 'string', + }, + stateDomain: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['fileUrl', 'stateDomain'], + }, + InteractionOutcome: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session_approval_decision', + }, + decision: { + $ref: '#/$defs/Decision', + }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'service_approval_decision', }, - binding: { - type: 'string', + decision: { + $ref: '#/$defs/Decision', }, - through: { + committedAt: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: [ - 'kind', - 'toolUseId', - 'toolName', - 'service', - 'method', - 'binding', - 'through', - ], + required: ['kind', 'decision', 'committedAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'permissions', - }, - toolUseId: { - type: ['string', 'null'], + const: 'permissions_decision', }, - basis: { - $ref: '#/$defs/PermissionBasis', + decision: { + $ref: '#/$defs/Decision2', }, - request: { - $ref: '#/$defs/PermissionRequest', + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'toolUseId', 'basis', 'request'], + required: ['kind', 'decision', 'committedAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'question', - }, - toolUseId: { - type: 'string', + const: 'question_answer', }, - questions: { + answers: { type: 'array', items: { - $ref: '#/$defs/InteractionQuestion', + type: ['string', 'null'], }, }, + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, }, additionalProperties: false, - required: ['kind', 'toolUseId', 'questions'], + required: ['kind', 'answers', 'committedAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'form', + const: 'form_answer', }, - toolUseId: { - type: 'string', + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - message: { - type: 'string', + }, + unevaluatedProperties: false, + oneOf: [ + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'accept', + }, + values: { + type: 'object', + additionalProperties: { + $ref: '#/$defs/FormValue', + }, + }, + }, + required: ['action', 'values'], }, - requester: { - $ref: '#/$defs/FormRequester', + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'decline', + }, + }, + required: ['action'], }, - fields: { - type: 'array', - items: { - $ref: '#/$defs/FormField', + { + type: 'object', + properties: { + action: { + type: 'string', + const: 'cancel', + }, }, + required: ['action'], }, - }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'message', 'requester', 'fields'], + ], + required: ['kind', 'committedAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'client_capability', - }, - toolUseId: { - type: 'string', + const: 'client_capability_decision', }, - target: { - $ref: '#/$defs/GrantTarget', + decision: { + $ref: '#/$defs/Decision', }, - basis: { - description: - 'Agent-owned approvals expire with this original permission boundary.\nUnbound capabilities are explicit; Agent-owned capabilities capture a full binding stamp.', - anyOf: [ - { - $ref: '#/$defs/PermissionBasis', - }, - { - type: 'null', - }, - ], + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'toolUseId', 'target'], + required: ['kind', 'decision', 'committedAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', + const: 'computer_app_decision', }, - toolUseId: { - type: 'string', + decision: { + $ref: '#/$defs/ComputerAppDecision', }, - nativeOperationId: { - type: 'string', + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - target: { - $ref: '#/$defs/GrantTarget', + }, + additionalProperties: false, + required: ['kind', 'decision', 'committedAt'], + }, + { + description: + 'Producer-committed once every requested system grant is present; no answer maps here.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'system_permission_granted', }, - basis: { - $ref: '#/$defs/PermissionBasis', + committedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - display: { - $ref: '#/$defs/ComputerAppDisplay', + }, + additionalProperties: false, + required: ['kind', 'committedAt'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'closure', }, - allowAlways: { - type: 'boolean', + reason: { + $ref: '#/$defs/ClosureReason', }, - authorityRevision: { + committedAt: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: [ - 'kind', - 'toolUseId', - 'nativeOperationId', - 'target', - 'basis', - 'display', - 'allowAlways', - 'authorityRevision', - ], + required: ['kind', 'reason', 'committedAt'], }, + ], + }, + Decision: { + type: 'string', + enum: ['allow', 'deny'], + }, + Decision2: { + oneOf: [ { - description: - 'Missing operating-system grants the waiting tool call needs. The producer\npolls the real grant state and commits `SystemPermissionGranted` itself;\nthe user can only cancel.', type: 'object', properties: { - kind: { + decision: { type: 'string', - const: 'system_permission', + const: 'deny', }, - toolUseId: { + }, + required: ['decision'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + decision: { type: 'string', + const: 'allow', }, - grants: { - description: 'Distinct missing grants.', - type: 'array', - items: { - $ref: '#/$defs/SystemPermissionGrant', - }, - minItems: 1, - }, - application: { - description: - 'The application name the operating system lists in its settings.', - type: 'string', - minLength: 1, + permissions: { + $ref: '#/$defs/Permissions', }, - applicationLocation: { - anyOf: [ - { - $ref: '#/$defs/SystemPermissionApplicationLocation', - }, - { - type: 'null', - }, - ], + scope: { + $ref: '#/$defs/Scope2', }, }, - additionalProperties: false, - required: ['kind', 'toolUseId', 'grants', 'application'], + additionalProperties: false, + required: ['decision', 'permissions', 'scope'], + }, + ], + }, + FormValue: { + anyOf: [ + { + type: 'string', + }, + { + type: 'number', + format: 'double', + }, + { + type: 'boolean', + }, + { + type: 'array', + items: { + type: 'string', + }, }, ], }, - PermissionBasis: { - description: - 'Captured authority, never silently upgraded by resolving a current binding.', + ComputerAppDecision: { + type: 'string', + enum: ['once', 'session', 'always', 'deny'], + }, + ClosureReason: { + type: 'string', + enum: [ + 'steered', + 'turn_stopped', + 'turn_terminal', + 'producer_cancelled', + 'timed_out', + 'host_restarted', + 'provider_disconnected', + ], + }, + AgentInteractionRejection: { + type: 'string', + enum: [ + 'not_your_child', + 'human_action_required', + 'authorization_changed', + 'permission_requests_disabled', + 'closed', + 'unavailable', + 'invalid_answer', + 'unsupported', + ], + }, + }, + }, + resources: {}, + }, + restoreSessionCheckpoint: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointRestore', + description: + 'Restore files into a new child of a selected directory and rebind the same,\nunchanged local Session. It never starts a model/tool call or rewinds history.', + type: 'object', + properties: { + checkpoint: { + $ref: '#/$defs/AgentHistoryCheckpointRead', + }, + parent: { + $ref: '#/$defs/AgentDirectorySelection', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + backend: { + $ref: '#/$defs/AgentBackendPreference', + }, + acceptExternalWritersRisk: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: [ + 'checkpoint', + 'parent', + 'permissions', + 'backend', + 'acceptExternalWritersRisk', + ], + $defs: { + AgentHistoryCheckpointRead: { type: 'object', properties: { - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, + sessionId: { + type: 'string', + minLength: 1, }, - binding: { - $ref: '#/$defs/BindingStamp', + revision: { + description: + 'Only the exact currently retained revision is readable; no latest fallback.', + type: 'string', + format: 'uuid', }, }, additionalProperties: false, - required: ['boundaryRevision', 'binding'], + required: ['sessionId', 'revision'], }, - BindingStamp: { + AgentDirectorySelection: { type: 'object', properties: { - id: { + rootId: { type: 'string', - format: 'uuid', + minLength: 1, }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, + segments: { + type: 'array', + items: { + type: 'string', + }, + }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['rootId', 'segments', 'directoryIdentity'], }, - SessionApprovalAnswer: { + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { description: - "Only access-expanding child decisions can require a parent's human confirmation.", + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'service_approval', - }, - decision: { - $ref: '#/$defs/ApprovalAllow', + const: 'model', }, - }, - additionalProperties: false, - required: ['kind', 'decision'], - }, - { - type: 'object', - properties: { - kind: { + connectionId: { type: 'string', - const: 'permissions', - }, - decision: { - $ref: '#/$defs/PermissionApproval', + minLength: 1, }, - }, - additionalProperties: false, - required: ['kind', 'decision'], - }, - { - type: 'object', - properties: { - kind: { + modelId: { type: 'string', - const: 'client_capability', + minLength: 1, }, - decision: { - $ref: '#/$defs/ApprovalAllow', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', + const: 'executor', }, - decision: { - $ref: '#/$defs/ComputerAppApproval', + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'decision'], + required: ['kind', 'selection'], }, ], }, - ApprovalAllow: { + ThinkingLevel: { type: 'string', - enum: ['allow'], + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - PermissionApproval: { - oneOf: [ - { - type: 'object', - properties: { - decision: { - type: 'string', - const: 'allow', - }, - permissions: { - $ref: '#/$defs/Permissions', + AgentExecutorSelection: { + type: 'object', + properties: { + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', }, - scope: { - $ref: '#/$defs/Scope2', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['decision', 'permissions', 'scope'], + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointRestored', + type: 'object', + properties: { + operation: { + type: 'string', + format: 'uuid', + }, + sessionId: { + type: 'string', + }, + sessionRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + workspace: { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + path: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['operation', 'sessionId', 'sessionRevision', 'head', 'workspace', 'path'], + $defs: { + AgentHistoryCheckpointHead: { + type: 'object', + properties: { + revision: { + type: 'string', + format: 'uuid', }, - ], + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', + }, + }, + additionalProperties: false, + required: ['revision', 'manifest'], }, - Permissions: { + AgentHistoryCheckpointObject: { type: 'object', properties: { - filesystem: { - type: 'array', - items: { - $ref: '#/$defs/Rule', - }, + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, - network: { - $ref: '#/$defs/Network', + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['filesystem', 'network'], + required: ['digest', 'bytes'], }, - Rule: { - description: - 'Paths have executor-native meaning. Validate and materialize them on the\nexecution host, never on a remote Desktop. This document alone grants nothing.', + AgentCheckpointWorkspaceSnapshot: { type: 'object', properties: { - path: { + identity: { type: 'string', }, - scope: { - $ref: '#/$defs/Scope', + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', }, - access: { - $ref: '#/$defs/Access', + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['path', 'scope', 'access'], - }, - Scope: { - type: 'string', - enum: ['subtree', 'exact'], + required: ['identity', 'consistency', 'warning', 'inventory'], }, - Access: { - description: - 'Equal-path conflict priority is different from authority breadth: deny wins.', - type: 'string', - enum: ['read', 'write', 'deny'], + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', + }, + ], }, - Network: { + AgentCheckpointWorkspaceWarning: { oneOf: [ { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', type: 'string', - enum: ['denied', 'allowed'], + const: 'external_writers_not_fenced', + }, + ], + }, + }, + }, + resources: {}, + }, + resumable: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentResumableRead', + description: + 'Observe the latest run using the model selected for a possible continuation.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + model: { + $ref: '#/$defs/ModelSelection', + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', }, { - type: 'object', - properties: { - restricted: { - type: 'object', - properties: { - destinations: { - type: 'array', - uniqueItems: true, - items: { - $ref: '#/$defs/Destination', - }, - }, - }, - additionalProperties: false, - required: ['destinations'], - }, - }, - required: ['restricted'], - additionalProperties: false, + type: 'null', }, ], }, - Destination: { + }, + additionalProperties: false, + required: ['sessionId', 'model'], + $defs: { + ModelSelection: { description: - 'Destination authority, independent of the upstream proxy used to reach it.\nA domain grant does not authorize its subdomains or other ports.', + 'Stable model identity, resolved from current connection settings at admission.', type: 'object', properties: { - host: { + connectionId: { type: 'string', minLength: 1, - maxLength: 253, }, - port: { - type: 'integer', - format: 'uint16', - minimum: 1, - maximum: 65535, + modelId: { + type: 'string', + minLength: 1, }, }, - additionalProperties: false, - required: ['host', 'port'], + required: ['connectionId', 'modelId'], }, - Scope2: { + ThinkingLevel: { type: 'string', - enum: ['once', 'turn', 'session'], + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - ComputerAppApproval: { - type: 'string', - enum: ['once', 'session', 'always'], + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentResumable', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'ready', + }, + source: { + $ref: '#/$defs/AgentExecution', + }, + }, + additionalProperties: false, + required: ['kind', 'source'], }, - PermissionRequest: { - description: - 'The Host supplies the real operation description; it is display information,\nnot authority inferred from a command string or a plugin-supplied Session ID.', + { type: 'object', properties: { + kind: { + type: 'string', + const: 'unavailable', + }, reason: { + $ref: '#/$defs/AgentResumeParkReason', + }, + }, + additionalProperties: false, + required: ['kind', 'reason'], + }, + ], + $defs: { + AgentExecution: { + description: + 'Canonical physical execution receipt, independent of the current Session tip.', + type: 'object', + properties: { + sessionId: { type: 'string', }, - command: { - anyOf: [ - { - $ref: '#/$defs/PermissionCommand', - }, - { - type: 'null', - }, - ], + turnId: { + type: 'string', }, - permissions: { - $ref: '#/$defs/Permissions', + runId: { + type: 'string', + }, + invocationId: { + type: 'string', + }, + highWater: { + description: 'Run-local event count, not the root-global transcript cursor.', + type: 'integer', + format: 'uint64', + minimum: 0, + }, + state: { + $ref: '#/$defs/AgentExecutionState', }, }, additionalProperties: false, - required: ['reason', 'permissions'], + required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], + }, + AgentExecutionState: { + type: 'string', + enum: [ + 'admitted', + 'running', + 'waiting', + 'paused', + 'completed', + 'failed', + 'cancelled', + ], + }, + AgentResumeParkReason: { + type: 'string', + enum: [ + 'closed', + 'archived', + 'busy', + 'source_missing', + 'source_changed', + 'source_not_resumable', + 'already_continued', + 'workspace_changed', + 'safety_check_failed', + ], + }, + }, + }, + resources: {}, + }, + resume: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentResume', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, }, - PermissionCommand: { - type: 'object', - properties: { - command: { - type: 'string', - }, - cwd: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['command', 'cwd'], + sourceInvocationId: { + type: 'string', + minLength: 1, }, - InteractionQuestion: { - type: 'object', - properties: { - question: { - type: 'string', - minLength: 1, + sourceHighWater: { + description: 'Exact Run-local count returned by execution.', + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + model: { + description: + 'Explicit model selection for this new continuation; original input is replayed.', + $ref: '#/$defs/ModelSelection', + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', }, - options: { - type: 'array', - items: { - $ref: '#/$defs/QuestionOption', - }, - minItems: 2, - maxItems: 3, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['question', 'options'], + ], }, - QuestionOption: { + }, + additionalProperties: false, + required: ['sessionId', 'sourceInvocationId', 'sourceHighWater', 'model'], + $defs: { + ModelSelection: { + description: + 'Stable model identity, resolved from current connection settings at admission.', type: 'object', properties: { - label: { + connectionId: { type: 'string', minLength: 1, }, - description: { + modelId: { type: 'string', minLength: 1, }, }, - additionalProperties: false, - required: ['label'], + required: ['connectionId', 'modelId'], }, - FormRequester: { - type: 'object', - properties: { - name: { - type: 'string', - }, - source: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['name'], + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - FormField: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentResumeResult', + oneOf: [ + { type: 'object', properties: { - name: { - type: 'string', - }, - label: { + kind: { type: 'string', + const: 'started', }, - required: { - type: 'boolean', - }, - description: { - type: ['string', 'null'], + execution: { + $ref: '#/$defs/AgentExecution', }, }, - required: ['name', 'label', 'required'], - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'string', - }, - default: { - type: ['string', 'null'], - }, - minLength: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - maxLength: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - format: { - anyOf: [ - { - $ref: '#/$defs/FormFormat', - }, - { - type: 'null', - }, - ], - }, - }, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'number', - }, - default: { - type: ['number', 'null'], - format: 'double', - }, - minimum: { - type: ['number', 'null'], - format: 'double', - }, - maximum: { - type: ['number', 'null'], - format: 'double', - }, - }, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'integer', - }, - default: { - type: ['number', 'null'], - format: 'double', - }, - minimum: { - type: ['number', 'null'], - format: 'double', - }, - maximum: { - type: ['number', 'null'], - format: 'double', - }, - }, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'boolean', - }, - default: { - type: ['boolean', 'null'], - }, - }, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'single_select', - }, - options: { - type: 'array', - items: { - $ref: '#/$defs/FormOption', - }, - }, - default: { - type: ['string', 'null'], - }, - }, - required: ['kind', 'options'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'multi_select', - }, - options: { - type: 'array', - items: { - $ref: '#/$defs/FormOption', - }, - }, - default: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - minItems: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - maxItems: { - type: ['integer', 'null'], - format: 'uint', - minimum: 0, - }, - }, - required: ['kind', 'options'], - }, - ], - }, - FormFormat: { - type: 'string', - enum: ['email', 'uri', 'date', 'date-time'], + additionalProperties: false, + required: ['kind', 'execution'], }, - FormOption: { + { type: 'object', properties: { - value: { + kind: { type: 'string', + const: 'parked', }, - label: { - type: 'string', + reason: { + $ref: '#/$defs/AgentResumeParkReason', }, }, additionalProperties: false, - required: ['value', 'label'], + required: ['kind', 'reason'], }, - GrantTarget: { + ], + $defs: { + AgentExecution: { + description: + 'Canonical physical execution receipt, independent of the current Session tip.', type: 'object', properties: { - providerId: { + sessionId: { type: 'string', }, - contractId: { + turnId: { type: 'string', }, - serverId: { + runId: { type: 'string', }, - toolName: { + invocationId: { type: 'string', }, - capability: { - $ref: '#/$defs/GrantCapability', + highWater: { + description: 'Run-local event count, not the root-global transcript cursor.', + type: 'integer', + format: 'uint64', + minimum: 0, }, - scope: { - $ref: '#/$defs/GrantScope', + state: { + $ref: '#/$defs/AgentExecutionState', }, }, - required: [ - 'providerId', - 'contractId', - 'serverId', - 'toolName', - 'capability', - 'scope', + additionalProperties: false, + required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], + }, + AgentExecutionState: { + type: 'string', + enum: [ + 'admitted', + 'running', + 'waiting', + 'paused', + 'completed', + 'failed', + 'cancelled', ], }, - GrantCapability: { - oneOf: [ - { + AgentResumeParkReason: { + type: 'string', + enum: [ + 'closed', + 'archived', + 'busy', + 'source_missing', + 'source_changed', + 'source_not_resumable', + 'already_continued', + 'workspace_changed', + 'safety_check_failed', + ], + }, + }, + }, + resources: {}, + }, + schedule: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentScheduleChange', + oneOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', - enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], + const: 'create', }, - { - description: - 'File download consent for a browser origin, independent of page access.', + input: { + $ref: '#/$defs/AgentScheduledInput', + }, + }, + additionalProperties: false, + required: ['kind', 'input'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', - const: 'browser_download', + const: 'update', }, - { - description: - "Read the browser profile's browsing history, independent of page access.", + taskId: { type: 'string', - const: 'browser_history', }, - { - description: - 'File upload consent for a browser origin, independent of reads and downloads.', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + input: { + $ref: '#/$defs/AgentScheduledInput', + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision', 'input'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', - const: 'browser_upload', + const: 'approve', }, - ], + taskId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], }, - GrantScope: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'browser_origin', - }, - origin: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'origin'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'pause', }, - { - description: - 'One selected browser profile generation; never an origin or app-wide grant.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'browser_profile', - }, - browserId: { - type: 'string', - }, - generation: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'browserId', 'generation'], + taskId: { + type: 'string', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'capability', - }, - }, - additionalProperties: false, - required: ['kind'], + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'mcp_tool', - }, - serverId: { - type: 'string', - }, - toolName: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'serverId', 'toolName'], + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'resume', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'computer_app', - }, - platform: { - $ref: '#/$defs/ComputerAppPlatform', - }, - applicationId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'platform', 'applicationId'], + taskId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'trigger_now', }, - ], - }, - ComputerAppPlatform: { - type: 'string', - enum: ['macos', 'windows'], + taskId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], }, - ComputerAppDisplay: { + { type: 'object', properties: { - name: { + kind: { type: 'string', + const: 'snooze', }, - appPath: { + taskId: { type: 'string', }, - risk: { - anyOf: [ - { - $ref: '#/$defs/ComputerAppRisk', - }, - { - type: 'null', - }, - ], + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - warning: { - type: ['string', 'null'], + delayMs: { + type: 'integer', + format: 'int64', }, }, additionalProperties: false, - required: ['name', 'appPath'], - }, - ComputerAppRisk: { - type: 'string', - enum: ['low', 'high'], + required: ['kind', 'taskId', 'revision', 'delayMs'], }, - SystemPermissionGrant: { - description: - 'A macOS privacy grant (TCC) that the Host process itself needs before native\nComputer Use can run. This is operating-system state the user changes in\nSystem Settings, not a Maka grant; the Host observes it and commits the outcome.', - oneOf: [ - { - description: 'System Settings > Privacy & Security > Accessibility.', + { + type: 'object', + properties: { + kind: { type: 'string', - const: 'accessibility', + const: 'acknowledge', }, - { - description: - 'System Settings > Privacy & Security > Screen & System Audio Recording.', + taskId: { type: 'string', - const: 'screen_recording', }, - ], + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + fireId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision', 'fireId'], }, - SystemPermissionApplicationLocation: { + { type: 'object', properties: { - fileUrl: { + kind: { type: 'string', + const: 'clear_history', }, - stateDomain: { + taskId: { type: 'string', }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, }, additionalProperties: false, - required: ['fileUrl', 'stateDomain'], + required: ['kind', 'taskId', 'revision'], }, - InteractionOutcome: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'session_approval_decision', - }, - decision: { - $ref: '#/$defs/Decision', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'delete', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'service_approval_decision', - }, - decision: { - $ref: '#/$defs/Decision', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + taskId: { + type: 'string', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'permissions_decision', - }, - decision: { - $ref: '#/$defs/Decision2', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'question_answer', - }, - answers: { - type: 'array', - items: { - type: ['string', 'null'], - }, - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'answers', 'committedAt'], + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], + }, + ], + $defs: { + AgentScheduledInput: { + type: 'object', + properties: { + task: { + $ref: '#/$defs/Create', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'form_answer', - }, - committedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - unevaluatedProperties: false, - oneOf: [ - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'accept', - }, - values: { - type: 'object', - additionalProperties: { - $ref: '#/$defs/FormValue', - }, - }, - }, - required: ['action', 'values'], - }, - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'decline', - }, - }, - required: ['action'], - }, - { - type: 'object', - properties: { - action: { - type: 'string', - const: 'cancel', - }, - }, - required: ['action'], - }, - ], - required: ['kind', 'committedAt'], + timezone: { + type: 'string', + }, + misfire: { + $ref: '#/$defs/Misfire', + }, + }, + additionalProperties: false, + required: ['task', 'timezone', 'misfire'], + }, + Create: { + type: 'object', + properties: { + title: { + type: 'string', + }, + intentBody: { + type: 'string', + }, + schedule: { + $ref: '#/$defs/Schedule', + }, + effect: { + $ref: '#/$defs/AgentScheduledEffect', + }, + maxFires: { + type: ['integer', 'null'], + format: 'uint32', + minimum: 0, + default: null, + }, + expiresAt: { + type: ['integer', 'null'], + format: 'int64', + default: null, }, + }, + additionalProperties: false, + required: ['title', 'intentBody', 'schedule', 'effect'], + }, + Schedule: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'client_capability_decision', - }, - decision: { - $ref: '#/$defs/Decision', + const: 'once', }, - committedAt: { + runAt: { type: 'integer', - format: 'uint64', - minimum: 0, + format: 'int64', }, }, additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + required: ['kind', 'runAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app_decision', - }, - decision: { - $ref: '#/$defs/ComputerAppDecision', + const: 'interval', }, - committedAt: { + everySeconds: { type: 'integer', - format: 'uint64', + format: 'uint32', minimum: 0, }, + startAt: { + type: 'integer', + format: 'int64', + }, }, additionalProperties: false, - required: ['kind', 'decision', 'committedAt'], + required: ['kind', 'everySeconds', 'startAt'], }, { - description: - 'Producer-committed once every requested system grant is present; no answer maps here.', type: 'object', properties: { kind: { type: 'string', - const: 'system_permission_granted', + const: 'calendar', }, - committedAt: { + recurrence: { + $ref: '#/$defs/Recurrence', + }, + anchorAt: { type: 'integer', - format: 'uint64', - minimum: 0, + format: 'int64', }, }, additionalProperties: false, - required: ['kind', 'committedAt'], + required: ['kind', 'recurrence', 'anchorAt'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'closure', + const: 'cron', }, - reason: { - $ref: '#/$defs/ClosureReason', + expression: { + type: 'string', }, - committedAt: { + startAt: { type: 'integer', - format: 'uint64', - minimum: 0, + format: 'int64', }, }, additionalProperties: false, - required: ['kind', 'reason', 'committedAt'], + required: ['kind', 'expression', 'startAt'], }, ], }, - Decision: { + Recurrence: { type: 'string', - enum: ['allow', 'deny'], + enum: ['daily', 'weekly', 'monthly'], }, - Decision2: { + AgentScheduledEffect: { oneOf: [ { type: 'object', properties: { - decision: { + kind: { type: 'string', - const: 'deny', + const: 'reminder', }, }, - required: ['decision'], + required: ['kind'], additionalProperties: false, }, { type: 'object', properties: { - decision: { + kind: { type: 'string', - const: 'allow', + const: 'session', }, - permissions: { - $ref: '#/$defs/Permissions', + target: { + $ref: '#/$defs/AgentScheduledTarget', }, - scope: { - $ref: '#/$defs/Scope2', + model: { + $ref: '#/$defs/ModelSelection', + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['decision', 'permissions', 'scope'], - }, - ], - }, - FormValue: { - anyOf: [ - { - type: 'string', - }, - { - type: 'number', - format: 'double', - }, - { - type: 'boolean', + required: ['kind', 'target', 'model'], }, { - type: 'array', - items: { - type: 'string', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'agent_run', + }, + target: { + $ref: '#/$defs/AgentScheduledTarget', + }, + model: { + $ref: '#/$defs/ModelSelection', + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, }, - }, - ], - }, - ComputerAppDecision: { - type: 'string', - enum: ['once', 'session', 'always', 'deny'], - }, - ClosureReason: { - type: 'string', - enum: [ - 'steered', - 'turn_stopped', - 'turn_terminal', - 'producer_cancelled', - 'timed_out', - 'host_restarted', - 'provider_disconnected', - ], - }, - AgentInteractionRejection: { - type: 'string', - enum: [ - 'not_your_child', - 'human_action_required', - 'authorization_changed', - 'permission_requests_disabled', - 'closed', - 'unavailable', - 'invalid_answer', - 'unsupported', - ], - }, - }, - }, - resources: {}, - }, - resumable: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentResumableRead', - description: - 'Observe the latest run using the model selected for a possible continuation.', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - model: { - $ref: '#/$defs/ModelSelection', - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['sessionId', 'model'], - $defs: { - ModelSelection: { - description: - 'Stable model identity, resolved from current connection settings at admission.', - type: 'object', - properties: { - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - }, - required: ['connectionId', 'modelId'], - }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentResumable', - oneOf: [ - { + additionalProperties: false, + required: ['kind', 'target', 'model'], + }, + ], + }, + AgentScheduledTarget: { + description: + 'Exact observed source boundary. This data is checked against the original\nSession; passing it never grants execution by itself.', type: 'object', properties: { - kind: { + sessionId: { type: 'string', - const: 'ready', }, - source: { - $ref: '#/$defs/AgentExecution', + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', }, }, additionalProperties: false, - required: ['kind', 'source'], + required: ['sessionId', 'workspace', 'permissions', 'boundaryRevision', 'binding'], }, - { + AgentWorkspace: { type: 'object', properties: { - kind: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], + }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { type: 'string', - const: 'unavailable', + minLength: 1, }, - reason: { - $ref: '#/$defs/AgentResumeParkReason', + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['path', 'directoryIdentity', 'identity'], }, - ], - $defs: { - AgentExecution: { - description: - 'Canonical physical execution receipt, independent of the current Session tip.', + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { - sessionId: { + id: { type: 'string', }, - turnId: { + common_dir: { type: 'string', }, - runId: { + source: { type: 'string', }, - invocationId: { + source_identity: { type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - highWater: { - description: 'Run-local event count, not the root-global transcript cursor.', - type: 'integer', - format: 'uint64', - minimum: 0, + base_commit: { + type: 'string', }, - state: { - $ref: '#/$defs/AgentExecutionState', + directory: { + type: 'string', }, }, additionalProperties: false, - required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], - }, - AgentExecutionState: { - type: 'string', - enum: [ - 'admitted', - 'running', - 'waiting', - 'paused', - 'completed', - 'failed', - 'cancelled', - ], - }, - AgentResumeParkReason: { - type: 'string', - enum: [ - 'closed', - 'archived', - 'busy', - 'source_missing', - 'source_changed', - 'source_not_resumable', - 'already_continued', - 'workspace_changed', - 'safety_check_failed', + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', ], }, - }, - }, - resources: {}, - }, - resume: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentResume', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - sourceInvocationId: { - type: 'string', - minLength: 1, - }, - sourceHighWater: { - description: 'Exact Run-local count returned by execution.', - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], }, - model: { + AgentWorkspaceAccess: { description: - 'Explicit model selection for this new continuation; original input is replayed.', - $ref: '#/$defs/ModelSelection', + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', }, - { - type: 'null', + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - ], + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], }, - }, - additionalProperties: false, - required: ['sessionId', 'sourceInvocationId', 'sourceHighWater', 'model'], - $defs: { ModelSelection: { description: 'Stable model identity, resolved from current connection settings at admission.', @@ -19171,191 +22136,225 @@ export const contracts = { type: 'string', enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, + Misfire: { + type: 'string', + enum: ['skip', 'latest'], + }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentResumeResult', + title: 'AgentScheduleResult', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'started', + const: 'committed', }, - execution: { - $ref: '#/$defs/AgentExecution', + taskId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'execution'], + required: ['kind', 'taskId', 'revision'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'parked', + const: 'conflict', + }, + actual: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', }, reason: { - $ref: '#/$defs/AgentResumeParkReason', + type: 'string', }, }, additionalProperties: false, required: ['kind', 'reason'], }, ], + }, + resources: {}, + }, + scheduleAlerts: { + mode: 'subscription', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentScheduleAlertsRead', + type: 'object', + additionalProperties: false, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentScheduleAlerts', + type: 'object', + properties: { + pending: { + type: 'integer', + format: 'uint32', + minimum: 0, + }, + reminders: { + type: 'array', + items: { + $ref: '#/$defs/AgentScheduledReminder', + }, + }, + }, + additionalProperties: false, + required: ['pending', 'reminders'], $defs: { - AgentExecution: { - description: - 'Canonical physical execution receipt, independent of the current Session tip.', + AgentScheduledReminder: { type: 'object', properties: { - sessionId: { + taskId: { type: 'string', }, - turnId: { + fireId: { type: 'string', }, - runId: { + title: { type: 'string', }, - invocationId: { + body: { type: 'string', }, - highWater: { - description: 'Run-local event count, not the root-global transcript cursor.', - type: 'integer', - format: 'uint64', - minimum: 0, - }, - state: { - $ref: '#/$defs/AgentExecutionState', - }, }, additionalProperties: false, - required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], - }, - AgentExecutionState: { - type: 'string', - enum: [ - 'admitted', - 'running', - 'waiting', - 'paused', - 'completed', - 'failed', - 'cancelled', - ], - }, - AgentResumeParkReason: { - type: 'string', - enum: [ - 'closed', - 'archived', - 'busy', - 'source_missing', - 'source_changed', - 'source_not_resumable', - 'already_continued', - 'workspace_changed', - 'safety_check_failed', - ], + required: ['taskId', 'fireId', 'title', 'body'], }, }, }, resources: {}, }, - schedule: { - mode: 'mutation', + schedules: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentScheduleChange', + title: 'AgentScheduleRead', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'create', + const: 'list', }, - input: { - $ref: '#/$defs/AgentScheduledInput', + cursor: { + type: ['string', 'null'], + }, + expectedRevision: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'input'], + required: ['kind'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'update', + const: 'get', }, taskId: { type: 'string', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - input: { - $ref: '#/$defs/AgentScheduledInput', - }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision', 'input'], + required: ['kind', 'taskId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'approve', - }, - taskId: { - type: 'string', + const: 'receipt', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + reference: { + $ref: '#/$defs/RecoveryRef', }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision'], + required: ['kind', 'reference'], }, - { + ], + $defs: { + RecoveryRef: { type: 'object', properties: { - kind: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { type: 'string', - const: 'pause', }, - taskId: { + method: { type: 'string', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + operation: { + type: 'string', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision'], + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - { + DeploymentRef: { type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'resume', }, - taskId: { + deployment: { type: 'string', + format: 'uuid', }, revision: { type: 'integer', @@ -19364,80 +22363,143 @@ export const contracts = { }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision'], + required: ['stateDomain', 'deployment', 'revision'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - kind: { - type: 'string', - const: 'trigger_now', - }, - taskId: { + name: { type: 'string', }, - revision: { + version: { type: 'integer', - format: 'uint64', + format: 'uint32', minimum: 0, }, + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', + }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision'], + required: ['name', 'version', 'transport', 'digest'], + }, + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSchedules', + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'snooze', - }, - taskId: { - type: 'string', + const: 'page', }, revision: { type: 'integer', format: 'uint64', minimum: 0, }, - delayMs: { - type: 'integer', - format: 'int64', + tasks: { + type: 'array', + items: { + $ref: '#/$defs/AgentScheduledSummary', + }, + }, + nextCursor: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision', 'delayMs'], + required: ['kind', 'revision', 'tasks'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'acknowledge', + const: 'task', }, - taskId: { + detail: { + anyOf: [ + { + $ref: '#/$defs/AgentScheduledDetail', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'changed', }, revision: { type: 'integer', format: 'uint64', minimum: 0, }, - fireId: { + }, + additionalProperties: false, + required: ['kind', 'revision'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'receipt', + }, + result: { + anyOf: [ + { + $ref: '#/$defs/AgentScheduleResult', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision', 'fireId'], + required: ['kind'], }, - { + ], + $defs: { + AgentScheduledSummary: { type: 'object', properties: { - kind: { - type: 'string', - const: 'clear_history', - }, - taskId: { + id: { type: 'string', }, revision: { @@ -19445,36 +22507,46 @@ export const contracts = { format: 'uint64', minimum: 0, }, + title: { + type: 'string', + }, + status: { + $ref: '#/$defs/Status', + }, + approved: { + type: 'boolean', + }, + nextFireAt: { + type: ['integer', 'null'], + format: 'int64', + }, + pendingFire: { + type: ['string', 'null'], + }, + reminder: { + type: 'boolean', + }, + lastError: { + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['kind', 'taskId', 'revision'], + required: ['id', 'revision', 'title', 'status', 'approved', 'reminder'], }, - { + Status: { + type: 'string', + enum: ['active', 'paused', 'completed', 'expired'], + }, + AgentScheduledDetail: { type: 'object', properties: { - kind: { - type: 'string', - const: 'delete', - }, - taskId: { - type: 'string', - }, revision: { type: 'integer', format: 'uint64', minimum: 0, }, - }, - additionalProperties: false, - required: ['kind', 'taskId', 'revision'], - }, - ], - $defs: { - AgentScheduledInput: { - type: 'object', - properties: { task: { - $ref: '#/$defs/Create', + $ref: '#/$defs/Task', }, timezone: { type: 'string', @@ -19482,39 +22554,112 @@ export const contracts = { misfire: { $ref: '#/$defs/Misfire', }, + approved: { + type: 'boolean', + }, + pendingFire: { + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['task', 'timezone', 'misfire'], + required: ['revision', 'task', 'timezone', 'misfire', 'approved'], }, - Create: { + Task: { type: 'object', properties: { - title: { + id: { type: 'string', }, - intentBody: { + title: { type: 'string', }, + intent: { + $ref: '#/$defs/Intent', + }, schedule: { $ref: '#/$defs/Schedule', }, effect: { $ref: '#/$defs/AgentScheduledEffect', }, + status: { + $ref: '#/$defs/Status', + }, + nextFireAt: { + type: ['integer', 'null'], + format: 'int64', + }, + lastFireAt: { + type: ['integer', 'null'], + format: 'int64', + }, + fireCount: { + type: 'integer', + format: 'uint32', + minimum: 0, + }, maxFires: { type: ['integer', 'null'], format: 'uint32', minimum: 0, - default: null, }, expiresAt: { type: ['integer', 'null'], format: 'int64', - default: null, + }, + createdBy: { + $ref: '#/$defs/Creator', + }, + createdAt: { + type: 'integer', + format: 'int64', + }, + updatedAt: { + type: 'integer', + format: 'int64', + }, + runs: { + type: 'array', + items: { + $ref: '#/$defs/Run', + }, + }, + lastError: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['title', 'intentBody', 'schedule', 'effect'], + required: [ + 'id', + 'title', + 'intent', + 'schedule', + 'effect', + 'status', + 'fireCount', + 'createdBy', + 'createdAt', + 'updatedAt', + 'runs', + ], + }, + Intent: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'text', + }, + body: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'body'], + }, + ], }, Schedule: { oneOf: [ @@ -19815,635 +22960,706 @@ export const contracts = { type: 'string', enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - Misfire: { - type: 'string', - enum: ['skip', 'latest'], + Creator: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'user', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'agent', + }, + sessionId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'system', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + ], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentScheduleResult', - oneOf: [ - { + Run: { type: 'object', properties: { - kind: { - type: 'string', - const: 'committed', - }, - taskId: { + id: { type: 'string', }, - revision: { + at: { type: 'integer', - format: 'uint64', - minimum: 0, + format: 'int64', }, - }, - additionalProperties: false, - required: ['kind', 'taskId', 'revision'], - }, - { - type: 'object', - properties: { - kind: { + outcome: { + $ref: '#/$defs/Outcome', + }, + message: { type: 'string', - const: 'conflict', }, - actual: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, + sessionId: { + type: ['string', 'null'], + }, + runId: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['kind'], + required: ['id', 'at', 'outcome', 'message'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'rejected', + Outcome: { + type: 'string', + enum: ['ok', 'failed', 'blocked'], + }, + Misfire: { + type: 'string', + enum: ['skip', 'latest'], + }, + AgentScheduleResult: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'committed', + }, + taskId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'taskId', 'revision'], }, - reason: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'conflict', + }, + actual: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind'], }, - }, - additionalProperties: false, - required: ['kind', 'reason'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', + }, + reason: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'reason'], + }, + ], }, - ], + }, }, resources: {}, }, - scheduleAlerts: { - mode: 'subscription', + search: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentScheduleAlertsRead', - type: 'object', - additionalProperties: false, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentScheduleAlerts', + title: 'AgentHistorySearch', type: 'object', properties: { - pending: { + sessionId: { + type: 'string', + minLength: 1, + }, + through: { + description: 'Original raw-event fence, shared with history reads.', type: 'integer', - format: 'uint32', + format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - reminders: { - type: 'array', - items: { - $ref: '#/$defs/AgentScheduledReminder', - }, + after: { + description: 'Exclusive transcript-row position; null starts a scan.', + anyOf: [ + { + $ref: '#/$defs/HistoryRowPosition', + }, + { + type: 'null', + }, + ], + }, + query: { + description: 'Literal query; no normalization.', + type: 'string', + minLength: 1, + }, + caseSensitive: { + description: + 'False uses Unicode simple case folding, without locale or multi-character folding.', + type: 'boolean', + }, + wholeWord: { + description: + 'Require no adjacent Unicode word character (Alphabetic, marks, decimal\ndigits, connector punctuation or join controls); not dictionary segmentation.', + type: 'boolean', + }, + includeInternal: { + type: 'boolean', }, }, additionalProperties: false, - required: ['pending', 'reminders'], + required: [ + 'sessionId', + 'through', + 'query', + 'caseSensitive', + 'wholeWord', + 'includeInternal', + ], $defs: { - AgentScheduledReminder: { + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - taskId: { - type: 'string', - }, - fireId: { - type: 'string', - }, - title: { - type: 'string', + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - body: { - type: 'string', + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['taskId', 'fireId', 'title', 'body'], + required: ['sourceSequence', 'itemIndex'], }, }, }, - resources: {}, - }, - schedules: { - mode: 'query', - input: { + output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentScheduleRead', + title: 'AgentHistorySearchPage', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'list', - }, - cursor: { - type: ['string', 'null'], + const: 'ready', }, - expectedRevision: { - type: ['integer', 'null'], + through: { + type: 'integer', format: 'uint64', minimum: 0, }, - }, - additionalProperties: false, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'get', - }, - taskId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'taskId'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'receipt', + matches: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryMatch', + }, + maxItems: 16, }, - reference: { - $ref: '#/$defs/RecoveryRef', + nextAfter: { + description: + 'Continue even when this batch has no matches. Null is the end of the scan.', + anyOf: [ + { + $ref: '#/$defs/HistoryRowPosition', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'reference'], + required: ['kind', 'through', 'matches', 'nextAfter'], }, ], $defs: { - RecoveryRef: { + AgentHistoryMatch: { type: 'object', properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - export: { - type: 'string', - }, - method: { - type: 'string', + field: { + description: + 'Exact canonical field to read through the source query; history body may omit it.', + $ref: '#/$defs/SourceField', }, - operation: { - type: 'string', - format: 'uuid', + position: { + description: + 'Exact transcript row. Open with HistoryPosition::Row at the same raw-event\nfence: Thinking for a thinking match, Body for every other source field.', + $ref: '#/$defs/HistoryRowPosition', }, - fingerprint: { + preview: { type: 'string', - }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + maxLength: 384, }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['field', 'position', 'preview'], }, - DeploymentRef: { - type: 'object', - properties: { - stateDomain: { + SourceField: { + description: + 'An explicitly selected canonical transcript field, never an execution grant.', + oneOf: [ + { type: 'string', + enum: [ + 'body', + 'thinking', + 'tool_name', + 'failure_message', + 'error_class', + 'arguments', + 'result', + ], }, - deployment: { + { + description: + 'The original service operation observation, separate from its returned value.', type: 'string', - format: 'uuid', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + const: 'receipt', }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + ], }, - RecoveryContract: { + HistoryRowPosition: { description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - name: { - type: 'string', + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - version: { + itemIndex: { type: 'integer', - format: 'uint32', + format: 'uint64', minimum: 0, - }, - transport: { - $ref: '#/$defs/Transport', - }, - digest: { - type: 'string', + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], + required: ['sourceSequence', 'itemIndex'], }, - Transport: { + }, + }, + resources: {}, + }, + selectBackend: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSelectBackend', + type: 'object', + properties: { + sessionId: { type: 'string', - enum: ['local', 'remote'], }, - RecoveryProtocol: { - type: 'string', - enum: ['journal_v1'], + backend: { + $ref: '#/$defs/AgentBackendPreference', }, }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSchedules', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'page', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - tasks: { - type: 'array', - items: { - $ref: '#/$defs/AgentScheduledSummary', + additionalProperties: false, + required: ['sessionId', 'backend'], + $defs: { + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', + }, + connectionId: { + type: 'string', + minLength: 1, + }, + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], }, - nextCursor: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['kind', 'revision', 'tasks'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'task', - }, - detail: { - anyOf: [ - { - $ref: '#/$defs/AgentScheduledDetail', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', }, - { - type: 'null', + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, - ], + }, + additionalProperties: false, + required: ['kind', 'selection'], }, - }, - additionalProperties: false, - required: ['kind'], + ], }, - { + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { type: 'object', properties: { - kind: { + executor: { type: 'string', - const: 'changed', + minLength: 1, }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + settings: { + $ref: '#/$defs/ExecutorSettings', }, }, additionalProperties: false, - required: ['kind', 'revision'], + required: ['executor', 'settings'], }, - { + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - kind: { - type: 'string', - const: 'receipt', + model: { + type: ['string', 'null'], }, - result: { + thinkingLevel: { anyOf: [ { - $ref: '#/$defs/AgentScheduleResult', + $ref: '#/$defs/ExecutorEffort', }, { type: 'null', }, ], }, + runtime: { + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['kind'], }, - ], - $defs: { - AgentScheduledSummary: { + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMetadataResult', + oneOf: [ + { type: 'object', properties: { - id: { - type: 'string', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - title: { + kind: { type: 'string', + const: 'committed', }, - status: { - $ref: '#/$defs/Status', - }, - approved: { - type: 'boolean', - }, - nextFireAt: { - type: ['integer', 'null'], - format: 'int64', - }, - pendingFire: { - type: ['string', 'null'], - }, - reminder: { - type: 'boolean', - }, - lastError: { - type: ['string', 'null'], + session: { + $ref: '#/$defs/AgentSession', }, }, additionalProperties: false, - required: ['id', 'revision', 'title', 'status', 'approved', 'reminder'], - }, - Status: { - type: 'string', - enum: ['active', 'paused', 'completed', 'expired'], + required: ['kind', 'session'], }, - AgentScheduledDetail: { + { type: 'object', properties: { - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - task: { - $ref: '#/$defs/Task', - }, - timezone: { + kind: { type: 'string', + const: 'revision_conflict', }, - misfire: { - $ref: '#/$defs/Misfire', - }, - approved: { - type: 'boolean', + expected: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - pendingFire: { - type: ['string', 'null'], + actual: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['revision', 'task', 'timezone', 'misfire', 'approved'], + required: ['kind', 'expected', 'actual'], }, - Task: { + { type: 'object', properties: { - id: { + kind: { type: 'string', + const: 'rejected', }, - title: { - type: 'string', + reason: { + $ref: '#/$defs/AgentRejection', }, - intent: { - $ref: '#/$defs/Intent', + }, + additionalProperties: false, + required: ['kind', 'reason'], + }, + ], + $defs: { + AgentSession: { + type: 'object', + properties: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], }, - schedule: { - $ref: '#/$defs/Schedule', + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], }, - effect: { - $ref: '#/$defs/AgentScheduledEffect', + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - status: { - $ref: '#/$defs/Status', + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, }, - nextFireAt: { - type: ['integer', 'null'], - format: 'int64', + dataSource: { + $ref: '#/$defs/AgentDataSource', }, - lastFireAt: { - type: ['integer', 'null'], - format: 'int64', + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', }, - fireCount: { + boundaryRevision: { type: 'integer', - format: 'uint32', + format: 'uint64', minimum: 0, }, - maxFires: { - type: ['integer', 'null'], - format: 'uint32', - minimum: 0, + binding: { + $ref: '#/$defs/BindingStamp', }, - expiresAt: { - type: ['integer', 'null'], - format: 'int64', + id: { + type: 'string', }, - createdBy: { - $ref: '#/$defs/Creator', + title: { + type: 'string', }, - createdAt: { + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { type: 'integer', - format: 'int64', + format: 'uint64', + minimum: 0, }, updatedAt: { type: 'integer', - format: 'int64', + format: 'uint64', + minimum: 0, }, - runs: { - type: 'array', - items: { - $ref: '#/$defs/Run', - }, + archived: { + type: 'boolean', }, - lastError: { + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', type: ['string', 'null'], }, + activity: { + $ref: '#/$defs/AgentSessionActivity', + }, }, additionalProperties: false, required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', 'id', 'title', - 'intent', - 'schedule', - 'effect', - 'status', - 'fireCount', - 'createdBy', - 'createdAt', + 'labels', + 'workspace', + 'revision', 'updatedAt', - 'runs', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', ], }, - Intent: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'text', - }, - body: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'body'], + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, }, - ], + position: { + $ref: '#/$defs/AgentForkPosition', + }, + }, + additionalProperties: false, + required: ['sessionId', 'position'], }, - Schedule: { + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', oneOf: [ { + description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'once', - }, - runAt: { - type: 'integer', - format: 'int64', + const: 'start', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'runAt'], }, { + description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'interval', - }, - everySeconds: { - type: 'integer', - format: 'uint32', - minimum: 0, - }, - startAt: { - type: 'integer', - format: 'int64', + const: 'end', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'everySeconds', 'startAt'], }, { + description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'calendar', - }, - recurrence: { - $ref: '#/$defs/Recurrence', + const: 'through_turn', }, - anchorAt: { - type: 'integer', - format: 'int64', + turnId: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'recurrence', 'anchorAt'], + required: ['kind', 'turnId'], }, { + description: + 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'cron', - }, - expression: { - type: 'string', - }, - startAt: { - type: 'integer', - format: 'int64', + const: 'before_turn', }, - }, - additionalProperties: false, - required: ['kind', 'expression', 'startAt'], - }, - ], - }, - Recurrence: { - type: 'string', - enum: ['daily', 'weekly', 'monthly'], - }, - AgentScheduledEffect: { - oneOf: [ - { - type: 'object', - properties: { - kind: { + turnId: { type: 'string', - const: 'reminder', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'turnId'], }, + ], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'session', + const: 'model', }, - target: { - $ref: '#/$defs/AgentScheduledTarget', + connectionId: { + type: 'string', + minLength: 1, }, - model: { - $ref: '#/$defs/ModelSelection', + modelId: { + type: 'string', + minLength: 1, }, thinkingLevel: { anyOf: [ @@ -20457,62 +23673,132 @@ export const contracts = { }, }, additionalProperties: false, - required: ['kind', 'target', 'model'], + required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'agent_run', - }, - target: { - $ref: '#/$defs/AgentScheduledTarget', - }, - model: { - $ref: '#/$defs/ModelSelection', + const: 'executor', }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'target', 'model'], + required: ['kind', 'selection'], }, ], }, - AgentScheduledTarget: { + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { + type: 'object', + properties: { + executor: { + type: 'string', + minLength: 1, + }, + settings: { + $ref: '#/$defs/ExecutorSettings', + }, + }, + additionalProperties: false, + required: ['executor', 'settings'], + }, + ExecutorSettings: { description: - 'Exact observed source boundary. This data is checked against the original\nSession; passing it never grants execution by itself.', + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - sessionId: { + model: { + type: ['string', 'null'], + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { type: 'string', }, - workspace: { - $ref: '#/$defs/AgentWorkspace', + deployment: { + type: 'string', + format: 'uuid', }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', + namespace: { + $ref: '#/$defs/InstanceId', }, - boundaryRevision: { + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'namespace'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'uuid', + }, + permissionEpoch: { type: 'integer', format: 'uint64', minimum: 0, }, - binding: { - $ref: '#/$defs/BindingStamp', - }, }, additionalProperties: false, - required: ['sessionId', 'workspace', 'permissions', 'boundaryRevision', 'binding'], + required: ['id', 'permissionEpoch'], }, AgentWorkspace: { type: 'object', @@ -20584,302 +23870,310 @@ export const contracts = { 'directory', ], }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['access', 'requestPermissions'], + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + AgentRejection: { type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], + enum: [ + 'permission_change_requires_user', + 'title_invalid', + 'labels_invalid', + 'tools_unavailable', + 'busy', + 'workspace_unavailable', + 'session_unavailable', + 'history_unavailable', + 'managed_session', + 'closed', + ], }, - BindingStamp: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'uuid', - }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['id', 'permissionEpoch'], + }, + }, + resources: {}, + }, + selectWorkspace: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentDirectoryPath', + description: + 'A directory on the Host: an absolute path, `~` standing for the home\ndirectory, within the configured workspace locations.', + type: 'object', + properties: { + path: { + type: 'string', + minLength: 1, }, - ModelSelection: { - description: - 'Stable model identity, resolved from current connection settings at admission.', + }, + additionalProperties: false, + required: ['path'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentWorkspace', + anyOf: [ + { + $ref: '#/$defs/AgentWorkspace', + }, + { + type: 'null', + }, + ], + $defs: { + AgentWorkspace: { type: 'object', properties: { - connectionId: { - type: 'string', - minLength: 1, + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], }, - modelId: { + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { type: 'string', minLength: 1, }, - }, - required: ['connectionId', 'modelId'], - }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - Creator: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'user', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'agent', - }, - sessionId: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'system', - }, - }, - required: ['kind'], - additionalProperties: false, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, - ], + }, + additionalProperties: false, + required: ['path', 'directoryIdentity', 'identity'], }, - Run: { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { id: { type: 'string', }, - at: { - type: 'integer', - format: 'int64', + common_dir: { + type: 'string', }, - outcome: { - $ref: '#/$defs/Outcome', + source: { + type: 'string', }, - message: { + source_identity: { type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - sessionId: { - type: ['string', 'null'], + base_commit: { + type: 'string', }, - runId: { - type: ['string', 'null'], + directory: { + type: 'string', }, }, additionalProperties: false, - required: ['id', 'at', 'outcome', 'message'], + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], }, - Outcome: { + }, + }, + resources: {}, + }, + send: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSend', + type: 'object', + properties: { + delivery: { + $ref: '#/$defs/AgentSendDelivery', + }, + expectedInvocationId: { + description: + 'An active send must not start new work after Stop fences its original Run.', + type: ['string', 'null'], + }, + attachments: { + description: + 'IDs of immutable user uploads in this Session; metadata is resolved by the owner.', + type: 'array', + items: { + type: 'string', + }, + default: [], + }, + backend: { + description: 'Omission uses the session’s saved next-turn preference.', + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], + }, + sessionId: { type: 'string', - enum: ['ok', 'failed', 'blocked'], + minLength: 1, }, - Misfire: { + message: { type: 'string', - enum: ['skip', 'latest'], }, - AgentScheduleResult: { + }, + additionalProperties: false, + required: ['sessionId', 'message'], + $defs: { + AgentSendDelivery: { + oneOf: [ + { + type: 'string', + enum: ['queue'], + }, + { + description: + 'Steer an active model, Codex or Claude turn; agy interrupts and resumes where supported. Other executors queue.', + type: 'string', + const: 'steer', + }, + ], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'committed', + const: 'model', }, - taskId: { + connectionId: { type: 'string', + minLength: 1, }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'taskId', 'revision'], - }, - { - type: 'object', - properties: { - kind: { + modelId: { type: 'string', - const: 'conflict', + minLength: 1, }, - actual: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind'], + required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'rejected', + const: 'executor', }, - reason: { - type: 'string', + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['kind', 'selection'], }, ], }, - }, - }, - resources: {}, - }, - search: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistorySearch', - type: 'object', - properties: { - sessionId: { + ThinkingLevel: { type: 'string', - minLength: 1, - }, - through: { - description: 'Original raw-event fence, shared with history reads.', - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - after: { - description: 'Exclusive transcript-row position; null starts a scan.', - anyOf: [ - { - $ref: '#/$defs/HistoryRowPosition', + AgentExecutorSelection: { + type: 'object', + properties: { + executor: { + type: 'string', + minLength: 1, }, - { - type: 'null', + settings: { + $ref: '#/$defs/ExecutorSettings', }, - ], - }, - query: { - description: 'Literal query; no normalization.', - type: 'string', - minLength: 1, - }, - caseSensitive: { - description: - 'False uses Unicode simple case folding, without locale or multi-character folding.', - type: 'boolean', - }, - wholeWord: { - description: - 'Require no adjacent Unicode word character (Alphabetic, marks, decimal\ndigits, connector punctuation or join controls); not dictionary segmentation.', - type: 'boolean', - }, - includeInternal: { - type: 'boolean', + }, + additionalProperties: false, + required: ['executor', 'settings'], }, - }, - additionalProperties: false, - required: [ - 'sessionId', - 'through', - 'query', - 'caseSensitive', - 'wholeWord', - 'includeInternal', - ], - $defs: { - HistoryRowPosition: { + ExecutorSettings: { description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + model: { + type: ['string', 'null'], }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistorySearchPage', + title: 'AgentSendResult', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'ready', + const: 'rejected', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, + sessionId: { + type: 'string', }, - matches: { - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryMatch', - }, - maxItems: 16, + reason: { + $ref: '#/$defs/AgentSendRejection', }, - nextAfter: { - description: - 'Continue even when this batch has no matches. Null is the end of the scan.', + skills: { anyOf: [ { - $ref: '#/$defs/HistoryRowPosition', + $ref: '#/$defs/SkillInvocationResult', }, { type: 'null', @@ -20888,852 +24182,991 @@ export const contracts = { }, }, additionalProperties: false, - required: ['kind', 'through', 'matches', 'nextAfter'], + required: ['kind', 'sessionId', 'reason'], }, - ], - $defs: { - AgentHistoryMatch: { + { type: 'object', properties: { - field: { - description: - 'Exact canonical field to read through the source query; history body may omit it.', - $ref: '#/$defs/SourceField', + kind: { + type: 'string', + const: 'started', }, - position: { - description: - 'Exact transcript row. Open with HistoryPosition::Row at the same raw-event\nfence: Thinking for a thinking match, Body for every other source field.', - $ref: '#/$defs/HistoryRowPosition', + sessionId: { + type: 'string', }, - preview: { + invocationId: { + type: 'string', + }, + messageId: { type: 'string', - maxLength: 384, }, }, additionalProperties: false, - required: ['field', 'position', 'preview'], + required: ['kind', 'sessionId', 'invocationId', 'messageId'], }, - SourceField: { - description: - 'An explicitly selected canonical transcript field, never an execution grant.', - oneOf: [ - { + { + type: 'object', + properties: { + kind: { type: 'string', - enum: [ - 'body', - 'thinking', - 'tool_name', - 'failure_message', - 'error_class', - 'arguments', - 'result', - ], + const: 'queued', }, - { - description: - 'The original service operation observation, separate from its returned value.', + sessionId: { type: 'string', - const: 'receipt', }, - ], + messageId: { + type: 'string', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId', 'messageId', 'revision'], }, - HistoryRowPosition: { + { description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + 'The caller may not send to this conversation: the Bot sends only to the\nworkers it created. Nothing was admitted.', type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + kind: { + type: 'string', + const: 'not_your_worker', }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + sessionId: { + type: 'string', }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['kind', 'sessionId'], }, - }, - }, - resources: {}, - }, - selectBackend: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSelectBackend', - type: 'object', - properties: { - sessionId: { + ], + $defs: { + AgentSendRejection: { type: 'string', + enum: ['model_unavailable', 'skills_unavailable', 'turn_stopped'], }, - backend: { - $ref: '#/$defs/AgentBackendPreference', + SkillInvocationResult: { + type: 'object', + properties: { + loaded: { + type: 'array', + items: { + $ref: '#/$defs/LoadedSkill', + }, + }, + failed: { + type: 'array', + items: { + $ref: '#/$defs/SkillInvocationFailure', + }, + }, + receipts: { + type: 'array', + items: { + $ref: '#/$defs/SkillInvocationReceipt', + }, + }, + }, + additionalProperties: false, + required: ['loaded', 'failed', 'receipts'], }, - }, - additionalProperties: false, - required: ['sessionId', 'backend'], - $defs: { - AgentBackendPreference: { + LoadedSkill: { + type: 'object', + properties: { + id: { + type: 'string', + }, + name: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['id', 'name'], + }, + SkillInvocationFailure: { + anyOf: [ + { + $ref: '#/$defs/SkillRequestFailure', + }, + ], + }, + SkillRequestFailure: { + type: 'object', + properties: { + request: { + type: 'string', + }, + reason: { + $ref: '#/$defs/SkillFailureReason', + }, + }, + additionalProperties: false, + required: ['request', 'reason'], + }, + SkillFailureReason: { + type: 'string', + enum: [ + 'invalid_name', + 'not_found', + 'disabled', + 'host_incompatible', + 'resolution_failed', + ], + }, + SkillInvocationReceipt: { description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ + 'Boolean wire discriminants are validated here, not retained as mutable\nbusiness fields beside a contradictory enum variant.', + anyOf: [ { type: 'object', properties: { - kind: { + invocation: { + $ref: '#/$defs/SkillInvocationMode', + }, + request: { + type: 'string', + }, + success: { + const: true, + }, + ref: { type: 'string', - const: 'model', }, - connectionId: { + id: { type: 'string', - minLength: 1, }, - modelId: { + name: { type: 'string', - minLength: 1, }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + scope: { + $ref: '#/$defs/SkillScope', + }, + source: { + $ref: '#/$defs/SkillSource', }, }, additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], + required: [ + 'invocation', + 'request', + 'success', + 'ref', + 'id', + 'name', + 'scope', + 'source', + ], }, { type: 'object', properties: { - kind: { + invocation: { + $ref: '#/$defs/SkillInvocationMode', + }, + request: { type: 'string', - const: 'executor', }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', + success: { + const: false, + }, + reason: { + $ref: '#/$defs/SkillFailureReason', }, }, additionalProperties: false, - required: ['kind', 'selection'], + required: ['invocation', 'request', 'success', 'reason'], }, ], }, - ThinkingLevel: { + SkillInvocationMode: { type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + enum: ['explicit', 'model_tool'], }, - AgentExecutorSelection: { + SkillScope: { + type: 'string', + enum: ['project', 'workspace', 'user', 'custom'], + }, + SkillSource: { + type: 'string', + enum: ['maka', 'agents', 'custom'], + }, + }, + }, + resources: {}, + }, + sessionCheckpointReceipt: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointReceiptRead', + type: 'object', + properties: { + reference: { + $ref: '#/$defs/RecoveryRef', + }, + input: { + $ref: '#/$defs/AgentSessionCheckpointPublish', + }, + }, + additionalProperties: false, + required: ['reference', 'input'], + $defs: { + RecoveryRef: { type: 'object', properties: { - executor: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { type: 'string', - minLength: 1, }, - settings: { - $ref: '#/$defs/ExecutorSettings', + method: { + type: 'string', + }, + operation: { + type: 'string', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['executor', 'settings'], + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - ExecutorSettings: { + DeploymentRef: { + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'revision'], + }, + InstanceId: { description: - 'Executor-owned model selection, independent of Host model connections.', + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - model: { - type: ['string', 'null'], + name: { + type: 'string', }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + version: { + type: 'integer', + format: 'uint32', + minimum: 0, }, - runtime: { - type: ['string', 'null'], + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', }, }, additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", + Transport: { type: 'string', + enum: ['local', 'remote'], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMetadataResult', - oneOf: [ - { + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + AgentSessionCheckpointPublish: { type: 'object', properties: { - kind: { - type: 'string', - const: 'committed', + history: { + $ref: '#/$defs/AgentHistoryCheckpointPublish', }, - session: { - $ref: '#/$defs/AgentSession', + workspaceConsistency: { + description: + 'Must be explicitly selected. Maka does not fence other applications.', + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', }, }, additionalProperties: false, - required: ['kind', 'session'], + required: ['history', 'workspaceConsistency'], }, - { + AgentHistoryCheckpointPublish: { type: 'object', properties: { - kind: { + sessionId: { type: 'string', - const: 'revision_conflict', + minLength: 1, }, - expected: { - type: 'integer', - format: 'uint64', - minimum: 0, + expectedRevision: { + type: ['string', 'null'], + format: 'uuid', }, - actual: { - type: 'integer', - format: 'uint64', - minimum: 0, + confirmedSubtreeDigest: { + description: + 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', + type: 'string', + pattern: '^[a-f0-9]{64}$', + }, + }, + additionalProperties: false, + required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], + }, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', + }, + ], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointReceipt', + type: 'object', + properties: { + operation: { + anyOf: [ + { + $ref: '#/$defs/OperationObservation', + }, + { + type: 'null', + }, + ], + }, + checkpoint: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionCheckpointPublication', + }, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'expected', 'actual'], + ], }, - { + }, + additionalProperties: false, + $defs: { + OperationObservation: { type: 'object', properties: { - kind: { - type: 'string', - const: 'rejected', + reference: { + $ref: '#/$defs/RecoveryRef', }, - reason: { - $ref: '#/$defs/AgentRejection', + outcome: { + $ref: '#/$defs/OperationOutcome', + }, + cleanup: { + $ref: '#/$defs/OperationCleanup', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['reference', 'outcome', 'cleanup'], }, - ], - $defs: { - AgentSession: { + RecoveryRef: { type: 'object', properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], + source: { + $ref: '#/$defs/DeploymentRef', }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], + namespace: { + $ref: '#/$defs/InstanceId', }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], + export: { + type: 'string', }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, + method: { + type: 'string', }, - dataSource: { - $ref: '#/$defs/AgentDataSource', + operation: { + type: 'string', + format: 'uuid', }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', + fingerprint: { + type: 'string', }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, + contract: { + $ref: '#/$defs/RecoveryContract', }, - binding: { - $ref: '#/$defs/BindingStamp', + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, - id: { + }, + additionalProperties: false, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], + }, + DeploymentRef: { + type: 'object', + properties: { + stateDomain: { type: 'string', }, - title: { + deployment: { type: 'string', - }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', + format: 'uuid', }, revision: { type: 'integer', format: 'uint64', minimum: 0, }, - updatedAt: { + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'revision'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + type: 'object', + properties: { + name: { + type: 'string', + }, + version: { type: 'integer', - format: 'uint64', + format: 'uint32', minimum: 0, }, - archived: { - type: 'boolean', + transport: { + $ref: '#/$defs/Transport', }, - pinned: { - type: 'boolean', + digest: { + type: 'string', }, - hasUnread: { - type: 'boolean', + }, + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], + }, + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + OperationOutcome: { + description: + 'Unresolved means no durable result is known, including after process loss.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unresolved', + }, + }, + required: ['kind'], + additionalProperties: false, }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'returned', + }, + value: true, + }, + additionalProperties: false, + required: ['kind', 'value'], }, - activity: { - $ref: '#/$defs/AgentSessionActivity', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unknown', + }, + diagnostic: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'diagnostic'], + }, + ], + }, + OperationCleanup: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unconfirmed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'confirmed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'failed', + }, + diagnostic: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'diagnostic'], }, - }, - additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', ], }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', + AgentSessionCheckpointPublication: { type: 'object', properties: { - sessionId: { - type: 'string', - minLength: 1, + publication: { + $ref: '#/$defs/AgentHistoryCheckpointPublication', }, - position: { - $ref: '#/$defs/AgentForkPosition', + workspaceConsistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + description: 'Stable warning, present even with a caller attestation.', + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', }, }, additionalProperties: false, - required: ['sessionId', 'position'], + required: ['publication', 'workspaceConsistency', 'warning'], }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', + AgentHistoryCheckpointPublication: { + type: 'object', + properties: { + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', + }, + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['state', 'cleanupPending'], + }, + AgentHistoryCheckpointState: { oneOf: [ { - description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'start', + const: 'capturing', }, }, required: ['kind'], additionalProperties: false, }, { - description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'end', + const: 'prepared', }, }, required: ['kind'], additionalProperties: false, }, { - description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'through_turn', + const: 'published', }, - turnId: { - type: 'string', - minLength: 1, + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind', 'head'], }, { - description: - 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'before_turn', + const: 'conflict', }, - turnId: { - type: 'string', - minLength: 1, + actual: { + type: ['string', 'null'], + format: 'uuid', }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind'], }, - ], - }, - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + const: 'interrupted', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', + const: 'unavailable', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'selection'], }, ], }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - AgentExecutorSelection: { - type: 'object', - properties: { - executor: { - type: 'string', - minLength: 1, - }, - settings: { - $ref: '#/$defs/ExecutorSettings', - }, - }, - additionalProperties: false, - required: ['executor', 'settings'], - }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', - type: 'object', - properties: { - model: { - type: ['string', 'null'], - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], - }, - runtime: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', - }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', - type: 'object', - properties: { - stateDomain: { - type: 'string', - }, - deployment: { - type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, - }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['access', 'requestPermissions'], - }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], - }, - BindingStamp: { + AgentHistoryCheckpointHead: { type: 'object', properties: { - id: { + revision: { type: 'string', format: 'uuid', }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['id', 'permissionEpoch'], - }, - AgentWorkspace: { - type: 'object', - properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, - path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: ['revision', 'manifest'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + AgentHistoryCheckpointObject: { type: 'object', properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { + digest: { type: 'string', + pattern: '^[a-f0-9]{64}$', }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - base_commit: { + }, + additionalProperties: false, + required: ['digest', 'bytes'], + }, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', type: 'string', + const: 'caller_confirmed_quiescent', }, - directory: { + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', type: 'string', + const: 'unverified', }, - }, - additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', ], }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], - }, - AgentRejection: { - type: 'string', - enum: [ - 'permission_change_requires_user', - 'title_invalid', - 'labels_invalid', - 'tools_unavailable', - 'busy', - 'workspace_unavailable', - 'session_unavailable', - 'history_unavailable', - 'managed_session', - 'closed', + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', + type: 'string', + const: 'external_writers_not_fenced', + }, ], }, }, }, resources: {}, }, - selectWorkspace: { - mode: 'mutation', + sessionCheckpointRestoreReceipt: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectoryPath', - description: - 'A directory on the Host: an absolute path, `~` standing for the home\ndirectory, within the configured workspace locations.', + title: 'AgentSessionCheckpointRestoreReceiptRead', type: 'object', properties: { - path: { - type: 'string', - minLength: 1, + reference: { + $ref: '#/$defs/RecoveryRef', + }, + input: { + $ref: '#/$defs/AgentSessionCheckpointRestore', }, }, additionalProperties: false, - required: ['path'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentWorkspace', - anyOf: [ - { - $ref: '#/$defs/AgentWorkspace', - }, - { - type: 'null', - }, - ], + required: ['reference', 'input'], $defs: { - AgentWorkspace: { + RecoveryRef: { type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], + source: { + $ref: '#/$defs/DeploymentRef', }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', + namespace: { + $ref: '#/$defs/InstanceId', }, - path: { + export: { type: 'string', - minLength: 1, }, - directoryIdentity: { + method: { type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', }, - identity: { + operation: { type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + DeploymentRef: { type: 'object', properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { + stateDomain: { type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', }, - base_commit: { + deployment: { type: 'string', + format: 'uuid', }, - directory: { - type: 'string', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - }, - }, - resources: {}, - }, - send: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSend', - type: 'object', - properties: { - delivery: { - $ref: '#/$defs/AgentSendDelivery', - }, - expectedInvocationId: { - description: - 'An active send must not start new work after Stop fences its original Run.', - type: ['string', 'null'], + required: ['stateDomain', 'deployment', 'revision'], }, - attachments: { + InstanceId: { description: - 'IDs of immutable user uploads in this Session; metadata is resolved by the owner.', + 'Hierarchical, stable source keys. Display aliases never become data identities.', type: 'array', items: { type: 'string', + format: 'uuid', }, - default: [], }, - backend: { - description: 'Omission uses the session’s saved next-turn preference.', - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + type: 'object', + properties: { + name: { + type: 'string', }, - { - type: 'null', + version: { + type: 'integer', + format: 'uint32', + minimum: 0, }, - ], + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], }, - sessionId: { + Transport: { type: 'string', - minLength: 1, + enum: ['local', 'remote'], }, - message: { + RecoveryProtocol: { type: 'string', + enum: ['journal_v1'], }, - }, - additionalProperties: false, - required: ['sessionId', 'message'], - $defs: { - AgentSendDelivery: { - oneOf: [ - { + AgentSessionCheckpointRestore: { + description: + 'Restore files into a new child of a selected directory and rebind the same,\nunchanged local Session. It never starts a model/tool call or rewinds history.', + type: 'object', + properties: { + checkpoint: { + $ref: '#/$defs/AgentHistoryCheckpointRead', + }, + parent: { + $ref: '#/$defs/AgentDirectorySelection', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + backend: { + $ref: '#/$defs/AgentBackendPreference', + }, + acceptExternalWritersRisk: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: [ + 'checkpoint', + 'parent', + 'permissions', + 'backend', + 'acceptExternalWritersRisk', + ], + }, + AgentHistoryCheckpointRead: { + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + revision: { + description: + 'Only the exact currently retained revision is readable; no latest fallback.', + type: 'string', + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['sessionId', 'revision'], + }, + AgentDirectorySelection: { + type: 'object', + properties: { + rootId: { type: 'string', - enum: ['queue'], + minLength: 1, }, - { - description: - 'Steer an active model, Codex or Claude turn; agy interrupts and resumes where supported. Other executors queue.', + segments: { + type: 'array', + items: { + type: 'string', + }, + }, + directoryIdentity: { type: 'string', - const: 'steer', + pattern: '^sha256:[a-f0-9]{64}$', }, - ], + }, + additionalProperties: false, + required: ['rootId', 'segments', 'directoryIdentity'], + }, + AgentWorkspacePermissions: { + type: 'object', + properties: { + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['access', 'requestPermissions'], + }, + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], }, AgentBackendPreference: { description: @@ -21818,260 +25251,359 @@ export const contracts = { { type: 'null', }, - ], + ], + }, + runtime: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + }, + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionCheckpointRestoreReceipt', + type: 'object', + properties: { + operation: { + anyOf: [ + { + $ref: '#/$defs/OperationObservation', + }, + { + type: 'null', + }, + ], + }, + restore: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionCheckpointRestored', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + $defs: { + OperationObservation: { + type: 'object', + properties: { + reference: { + $ref: '#/$defs/RecoveryRef', + }, + outcome: { + $ref: '#/$defs/OperationOutcome', + }, + cleanup: { + $ref: '#/$defs/OperationCleanup', + }, + }, + additionalProperties: false, + required: ['reference', 'outcome', 'cleanup'], + }, + RecoveryRef: { + type: 'object', + properties: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { + type: 'string', + }, + method: { + type: 'string', + }, + operation: { + type: 'string', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', + }, + }, + additionalProperties: false, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], + }, + DeploymentRef: { + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'revision'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + type: 'object', + properties: { + name: { + type: 'string', + }, + version: { + type: 'integer', + format: 'uint32', + minimum: 0, + }, + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], + }, + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + OperationOutcome: { + description: + 'Unresolved means no durable result is known, including after process loss.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unresolved', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'returned', + }, + value: true, + }, + additionalProperties: false, + required: ['kind', 'value'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unknown', + }, + diagnostic: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'diagnostic'], + }, + ], + }, + OperationCleanup: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unconfirmed', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'confirmed', + }, + }, + required: ['kind'], + additionalProperties: false, }, - runtime: { - type: ['string', 'null'], + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'failed', + }, + diagnostic: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'diagnostic'], }, - }, - additionalProperties: false, - }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', + ], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSendResult', - oneOf: [ - { + AgentSessionCheckpointRestored: { type: 'object', properties: { - kind: { + operation: { type: 'string', - const: 'rejected', + format: 'uuid', }, sessionId: { type: 'string', }, - reason: { - $ref: '#/$defs/AgentSendRejection', + sessionRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - skills: { - anyOf: [ - { - $ref: '#/$defs/SkillInvocationResult', - }, - { - type: 'null', - }, - ], + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + workspace: { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + path: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'reason'], + required: [ + 'operation', + 'sessionId', + 'sessionRevision', + 'head', + 'workspace', + 'path', + ], }, - { + AgentHistoryCheckpointHead: { type: 'object', properties: { - kind: { - type: 'string', - const: 'started', - }, - sessionId: { - type: 'string', - }, - invocationId: { + revision: { type: 'string', + format: 'uuid', }, - messageId: { - type: 'string', + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'invocationId', 'messageId'], + required: ['revision', 'manifest'], }, - { + AgentHistoryCheckpointObject: { type: 'object', properties: { - kind: { - type: 'string', - const: 'queued', - }, - sessionId: { - type: 'string', - }, - messageId: { + digest: { type: 'string', + pattern: '^[a-f0-9]{64}$', }, - revision: { + bytes: { type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'messageId', 'revision'], + required: ['digest', 'bytes'], }, - { - description: - 'The caller may not send to this conversation: the Bot sends only to the\nworkers it created. Nothing was admitted.', + AgentCheckpointWorkspaceSnapshot: { type: 'object', properties: { - kind: { - type: 'string', - const: 'not_your_worker', - }, - sessionId: { + identity: { type: 'string', }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], - }, - ], - $defs: { - AgentSendRejection: { - type: 'string', - enum: ['model_unavailable', 'skills_unavailable', 'turn_stopped'], - }, - SkillInvocationResult: { - type: 'object', - properties: { - loaded: { - type: 'array', - items: { - $ref: '#/$defs/LoadedSkill', - }, + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', }, - failed: { - type: 'array', - items: { - $ref: '#/$defs/SkillInvocationFailure', - }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', }, - receipts: { - type: 'array', - items: { - $ref: '#/$defs/SkillInvocationReceipt', - }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['loaded', 'failed', 'receipts'], + required: ['identity', 'consistency', 'warning', 'inventory'], }, - LoadedSkill: { - type: 'object', - properties: { - id: { - type: 'string', - }, - name: { + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', type: 'string', + const: 'caller_confirmed_quiescent', }, - }, - additionalProperties: false, - required: ['id', 'name'], - }, - SkillInvocationFailure: { - anyOf: [ { - $ref: '#/$defs/SkillRequestFailure', - }, - ], - }, - SkillRequestFailure: { - type: 'object', - properties: { - request: { + description: + 'Files may not represent one instant; successful digest checks do not change this.', type: 'string', + const: 'unverified', }, - reason: { - $ref: '#/$defs/SkillFailureReason', - }, - }, - additionalProperties: false, - required: ['request', 'reason'], - }, - SkillFailureReason: { - type: 'string', - enum: [ - 'invalid_name', - 'not_found', - 'disabled', - 'host_incompatible', - 'resolution_failed', ], }, - SkillInvocationReceipt: { - description: - 'Boolean wire discriminants are validated here, not retained as mutable\nbusiness fields beside a contradictory enum variant.', - anyOf: [ - { - type: 'object', - properties: { - invocation: { - $ref: '#/$defs/SkillInvocationMode', - }, - request: { - type: 'string', - }, - success: { - const: true, - }, - ref: { - type: 'string', - }, - id: { - type: 'string', - }, - name: { - type: 'string', - }, - scope: { - $ref: '#/$defs/SkillScope', - }, - source: { - $ref: '#/$defs/SkillSource', - }, - }, - additionalProperties: false, - required: [ - 'invocation', - 'request', - 'success', - 'ref', - 'id', - 'name', - 'scope', - 'source', - ], - }, + AgentCheckpointWorkspaceWarning: { + oneOf: [ { - type: 'object', - properties: { - invocation: { - $ref: '#/$defs/SkillInvocationMode', - }, - request: { - type: 'string', - }, - success: { - const: false, - }, - reason: { - $ref: '#/$defs/SkillFailureReason', - }, - }, - additionalProperties: false, - required: ['invocation', 'request', 'success', 'reason'], + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', + type: 'string', + const: 'external_writers_not_fenced', }, ], }, - SkillInvocationMode: { - type: 'string', - enum: ['explicit', 'model_tool'], - }, - SkillScope: { - type: 'string', - enum: ['project', 'workspace', 'user', 'custom'], - }, - SkillSource: { - type: 'string', - enum: ['maka', 'agents', 'custom'], - }, }, }, resources: {}, diff --git a/packages/plugin-sdk/src/generated/methods.ts b/packages/plugin-sdk/src/generated/methods.ts index 9f66a77d173..329e4b12ca5 100644 --- a/packages/plugin-sdk/src/generated/methods.ts +++ b/packages/plugin-sdk/src/generated/methods.ts @@ -37,6 +37,61 @@ export type AgentCompactOutput_AgentCompactRejection = /** * Hierarchical, stable source keys. Display aliases never become data identities. */ +export type AgentHistoryCheckpointLoadReceiptInput_InstanceId = string[]; +export type AgentHistoryCheckpointLoadReceiptInput_Transport = 'local' | 'remote'; +export type AgentHistoryCheckpointLoadReceiptInput_RecoveryProtocol = 'journal_v1'; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ +export type AgentHistoryCheckpointLoadReceiptOutput_InstanceId = string[]; +export type AgentHistoryCheckpointLoadReceiptOutput_Transport = 'local' | 'remote'; +export type AgentHistoryCheckpointLoadReceiptOutput_RecoveryProtocol = 'journal_v1'; +/** + * Unresolved means no durable result is known, including after process loss. + */ +export type AgentHistoryCheckpointLoadReceiptOutput_OperationOutcome = + | { + kind: 'unresolved'; + } + | { + kind: 'returned'; + value: unknown; + } + | { + kind: 'unknown'; + diagnostic: string; + }; +export type AgentHistoryCheckpointLoadReceiptOutput_OperationCleanup = + | { + kind: 'unconfirmed'; + } + | { + kind: 'confirmed'; + } + | { + kind: 'failed'; + diagnostic: string; + }; +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoadState = + | { + kind: 'preparing'; + } + | { + kind: 'ready'; + sessionIds: string[]; + workspace?: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceSnapshot | null; + } + | { + kind: 'unavailable'; + }; +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ export type AgentHistoryCheckpointReceiptInput_InstanceId = string[]; export type AgentHistoryCheckpointReceiptInput_Transport = 'local' | 'remote'; export type AgentHistoryCheckpointReceiptInput_RecoveryProtocol = 'journal_v1'; @@ -93,6 +148,56 @@ export type AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointState = | { kind: 'unavailable'; }; +export type AgentLoadHistoryCheckpointOutput_AgentCheckpointLoadState = + | { + kind: 'preparing'; + } + | { + kind: 'ready'; + sessionIds: string[]; + workspace?: AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot | null; + } + | { + kind: 'unavailable'; + }; +export type AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; +/** + * Row reveals an exact field; Before/After continue at byte boundaries and + * Event addresses the raw event log. Returned chunks retain ascending source + * order, including a reverse window. + */ +export type AgentLoadedHistoryCheckpointHistoryInput_HistoryPosition = + | { + kind: 'start'; + } + | { + kind: 'tail'; + } + | { + kind: 'row'; + position: AgentLoadedHistoryCheckpointHistoryInput_HistoryRowPosition; + field: AgentLoadedHistoryCheckpointHistoryInput_HistoryField; + } + | { + kind: 'before'; + cursor: AgentLoadedHistoryCheckpointHistoryInput_Cursor; + } + | { + kind: 'after'; + cursor: AgentLoadedHistoryCheckpointHistoryInput_Cursor; + } + | { + kind: 'event'; + sequence: number; + }; +/** + * A readable field in a conversation window, in presentation order. + */ +export type AgentLoadedHistoryCheckpointHistoryInput_HistoryField = 'thinking' | 'body'; export type AgentOpenBotOutput_AgentBotRejection = | 'disabled' | 'creation_rejected' @@ -119,6 +224,35 @@ export type AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointState = | { kind: 'unavailable'; }; +export type AgentPublishSessionCheckpointInput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState = + | { + kind: 'capturing'; + } + | { + kind: 'prepared'; + } + | { + kind: 'published'; + head: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointHead; + } + | { + kind: 'conflict'; + actual?: string | null; + } + | { + kind: 'interrupted'; + } + | { + kind: 'unavailable'; + }; +export type AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; /** * All parts are subscription items; only End admits a complete preview. * Offsets and byte counts refer to original bytes, not Base64 or UTF-16 lengths. @@ -166,6 +300,13 @@ export type AgentReadFileOutput_AgentFileContent = }; export type AgentReadFileOutput_AgentFileKind = 'file' | 'directory' | 'symlink' | 'other'; export type AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointFrame = + | { + kind: 'native_session_v1'; + head: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead; + inventory: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointInventory; + bundle: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointObject; + workspace: AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot; + } | { kind: 'native_history_v1'; head: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead; @@ -182,6 +323,198 @@ export type AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointFrame = digest: string; bytes: number; }; +export type AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; +/** + * Session sandbox policy. Full access disables isolation and permits Computer Use + * without additional application, browser or audio consent. + */ +export type AgentRestoreSessionCheckpointInput_AgentWorkspaceAccess = + | 'read_only' + | 'workspace_write' + | 'danger_full_access'; +/** + * The next turn's choice, independent of an already admitted execution. + * Model revisions are resolved afresh; preferences never grant execution authority. + */ +export type AgentRestoreSessionCheckpointInput_AgentBackendPreference = + | { + kind: 'model'; + connectionId: string; + modelId: string; + thinkingLevel?: AgentRestoreSessionCheckpointInput_ThinkingLevel | null; + } + | { + kind: 'executor'; + selection: AgentRestoreSessionCheckpointInput_AgentExecutorSelection; + }; +export type AgentRestoreSessionCheckpointInput_ThinkingLevel = + | 'off' + | 'minimal' + | 'low' + | 'medium' + | 'high' + | 'xhigh' + | 'max' + | 'ultra'; +/** + * Runtime-native effort, not a Host model's closed reasoning enum. + */ +export type AgentRestoreSessionCheckpointInput_ExecutorEffort = string; +export type AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ +export type AgentSessionCheckpointReceiptInput_InstanceId = string[]; +export type AgentSessionCheckpointReceiptInput_Transport = 'local' | 'remote'; +export type AgentSessionCheckpointReceiptInput_RecoveryProtocol = 'journal_v1'; +export type AgentSessionCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ +export type AgentSessionCheckpointReceiptOutput_InstanceId = string[]; +export type AgentSessionCheckpointReceiptOutput_Transport = 'local' | 'remote'; +export type AgentSessionCheckpointReceiptOutput_RecoveryProtocol = 'journal_v1'; +/** + * Unresolved means no durable result is known, including after process loss. + */ +export type AgentSessionCheckpointReceiptOutput_OperationOutcome = + | { + kind: 'unresolved'; + } + | { + kind: 'returned'; + value: unknown; + } + | { + kind: 'unknown'; + diagnostic: string; + }; +export type AgentSessionCheckpointReceiptOutput_OperationCleanup = + | { + kind: 'unconfirmed'; + } + | { + kind: 'confirmed'; + } + | { + kind: 'failed'; + diagnostic: string; + }; +export type AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointState = + | { + kind: 'capturing'; + } + | { + kind: 'prepared'; + } + | { + kind: 'published'; + head: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointHead; + } + | { + kind: 'conflict'; + actual?: string | null; + } + | { + kind: 'interrupted'; + } + | { + kind: 'unavailable'; + }; +export type AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ +export type AgentSessionCheckpointRestoreReceiptInput_InstanceId = string[]; +export type AgentSessionCheckpointRestoreReceiptInput_Transport = 'local' | 'remote'; +export type AgentSessionCheckpointRestoreReceiptInput_RecoveryProtocol = 'journal_v1'; +/** + * Session sandbox policy. Full access disables isolation and permits Computer Use + * without additional application, browser or audio consent. + */ +export type AgentSessionCheckpointRestoreReceiptInput_AgentWorkspaceAccess = + | 'read_only' + | 'workspace_write' + | 'danger_full_access'; +/** + * The next turn's choice, independent of an already admitted execution. + * Model revisions are resolved afresh; preferences never grant execution authority. + */ +export type AgentSessionCheckpointRestoreReceiptInput_AgentBackendPreference = + | { + kind: 'model'; + connectionId: string; + modelId: string; + thinkingLevel?: AgentSessionCheckpointRestoreReceiptInput_ThinkingLevel | null; + } + | { + kind: 'executor'; + selection: AgentSessionCheckpointRestoreReceiptInput_AgentExecutorSelection; + }; +export type AgentSessionCheckpointRestoreReceiptInput_ThinkingLevel = + | 'off' + | 'minimal' + | 'low' + | 'medium' + | 'high' + | 'xhigh' + | 'max' + | 'ultra'; +/** + * Runtime-native effort, not a Host model's closed reasoning enum. + */ +export type AgentSessionCheckpointRestoreReceiptInput_ExecutorEffort = string; +/** + * Hierarchical, stable source keys. Display aliases never become data identities. + */ +export type AgentSessionCheckpointRestoreReceiptOutput_InstanceId = string[]; +export type AgentSessionCheckpointRestoreReceiptOutput_Transport = 'local' | 'remote'; +export type AgentSessionCheckpointRestoreReceiptOutput_RecoveryProtocol = 'journal_v1'; +/** + * Unresolved means no durable result is known, including after process loss. + */ +export type AgentSessionCheckpointRestoreReceiptOutput_OperationOutcome = + | { + kind: 'unresolved'; + } + | { + kind: 'returned'; + value: unknown; + } + | { + kind: 'unknown'; + diagnostic: string; + }; +export type AgentSessionCheckpointRestoreReceiptOutput_OperationCleanup = + | { + kind: 'unconfirmed'; + } + | { + kind: 'confirmed'; + } + | { + kind: 'failed'; + diagnostic: string; + }; +export type AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; export type SettingsAssistantPreferencesOutput_LanguagePreference = | 'auto' | 'en' @@ -589,6 +922,17 @@ export interface ServiceMethods { input: Runtime.AgentSessionRead; output: AgentHistoryCheckpointHeadOutput_AgentHistoryCheckpointHead | null; }; + historyCheckpointLoadReceipt: { + mode: 'query'; + input: { + reference: AgentHistoryCheckpointLoadReceiptInput_RecoveryRef; + input: AgentHistoryCheckpointLoadReceiptInput_AgentHistoryCheckpointRead; + }; + output: { + operation?: AgentHistoryCheckpointLoadReceiptOutput_OperationObservation | null; + load?: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoad | null; + }; + }; historyCheckpointReceipt: { mode: 'query'; input: { @@ -650,6 +994,53 @@ export interface ServiceMethods { input: Runtime.AgentFileListRead; output: Runtime.AgentFilePage; }; + loadHistoryCheckpoint: { + mode: 'mutation'; + input: { + sessionId: string; + /** + * Only the exact currently retained revision is readable; no latest fallback. + */ + revision: string; + }; + output: { + head: AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointHead; + inspection: AgentLoadHistoryCheckpointOutput_AgentCheckpointInspection; + state: AgentLoadHistoryCheckpointOutput_AgentCheckpointLoadState; + }; + }; + loadedHistoryCheckpointArtifact: { + mode: 'query'; + input: { + inspection: AgentLoadedHistoryCheckpointArtifactInput_AgentCheckpointInspection; + sessionId: string; + artifactId: string; + offset: number; + }; + output: AgentLoadedHistoryCheckpointArtifactOutput_AgentCheckpointArtifactChunk | null; + }; + loadedHistoryCheckpointArtifacts: { + mode: 'query'; + input: { + inspection: AgentLoadedHistoryCheckpointArtifactsInput_AgentCheckpointInspection; + sessionId: string; + offset: number; + }; + output: { + artifacts: AgentLoadedHistoryCheckpointArtifactsOutput_AgentCheckpointArtifact[]; + nextOffset: number | null; + }; + }; + loadedHistoryCheckpointHistory: { + mode: 'query'; + input: { + inspection: AgentLoadedHistoryCheckpointHistoryInput_AgentCheckpointInspection; + sessionId: string; + position: AgentLoadedHistoryCheckpointHistoryInput_HistoryPosition; + includeThinking?: boolean; + }; + output: Runtime.AgentHistoryPage; + }; manageMcpServer: { mode: 'mutation'; input: Runtime.AgentMcpConfigurationChange; @@ -782,6 +1173,24 @@ export interface ServiceMethods { cleanupPending: boolean; }; }; + publishSessionCheckpoint: { + mode: 'mutation'; + input: { + history: AgentPublishSessionCheckpointInput_AgentHistoryCheckpointPublish; + /** + * Must be explicitly selected. Maka does not fence other applications. + */ + workspaceConsistency: AgentPublishSessionCheckpointInput_AgentCheckpointWorkspaceConsistency; + }; + output: { + publication: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointPublication; + workspaceConsistency: AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency; + /** + * Stable warning, present even with a caller attestation. + */ + warning: AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceWarning; + }; + }; queue: { mode: 'query'; input: Runtime.AgentSessionRead; @@ -848,6 +1257,28 @@ export interface ServiceMethods { input: Runtime.AgentInteractionRespond; output: Runtime.AgentInteractionResult; }; + restoreSessionCheckpoint: { + mode: 'mutation'; + /** + * Restore files into a new child of a selected directory and rebind the same, + * unchanged local Session. It never starts a model/tool call or rewinds history. + */ + input: { + checkpoint: AgentRestoreSessionCheckpointInput_AgentHistoryCheckpointRead; + parent: AgentRestoreSessionCheckpointInput_AgentDirectorySelection; + permissions: AgentRestoreSessionCheckpointInput_AgentWorkspacePermissions; + backend: AgentRestoreSessionCheckpointInput_AgentBackendPreference; + acceptExternalWritersRisk: boolean; + }; + output: { + operation: string; + sessionId: string; + sessionRevision: number; + head: AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointHead; + workspace: AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceSnapshot; + path: string; + }; + }; resumable: { mode: 'query'; input: Runtime.AgentResumableRead; @@ -899,6 +1330,28 @@ export interface ServiceMethods { input: Runtime.AgentSend; output: Runtime.AgentSendResult; }; + sessionCheckpointReceipt: { + mode: 'query'; + input: { + reference: AgentSessionCheckpointReceiptInput_RecoveryRef; + input: AgentSessionCheckpointReceiptInput_AgentSessionCheckpointPublish; + }; + output: { + operation?: AgentSessionCheckpointReceiptOutput_OperationObservation | null; + checkpoint?: AgentSessionCheckpointReceiptOutput_AgentSessionCheckpointPublication | null; + }; + }; + sessionCheckpointRestoreReceipt: { + mode: 'query'; + input: { + reference: AgentSessionCheckpointRestoreReceiptInput_RecoveryRef; + input: AgentSessionCheckpointRestoreReceiptInput_AgentSessionCheckpointRestore; + }; + output: { + operation?: AgentSessionCheckpointRestoreReceiptOutput_OperationObservation | null; + restore?: AgentSessionCheckpointRestoreReceiptOutput_AgentSessionCheckpointRestored | null; + }; + }; setCompactionPrompt: { mode: 'mutation'; input: { @@ -1798,6 +2251,91 @@ export interface AgentHistoryCheckpointHeadOutput_AgentHistoryCheckpointObject { digest: string; bytes: number; } +export interface AgentHistoryCheckpointLoadReceiptInput_RecoveryRef { + source: AgentHistoryCheckpointLoadReceiptInput_DeploymentRef; + namespace: AgentHistoryCheckpointLoadReceiptInput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentHistoryCheckpointLoadReceiptInput_RecoveryContract; + recovery: AgentHistoryCheckpointLoadReceiptInput_RecoveryProtocol; +} +export interface AgentHistoryCheckpointLoadReceiptInput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentHistoryCheckpointLoadReceiptInput_RecoveryContract { + name: string; + version: number; + transport: AgentHistoryCheckpointLoadReceiptInput_Transport; + digest: string; +} +export interface AgentHistoryCheckpointLoadReceiptInput_AgentHistoryCheckpointRead { + sessionId: string; + /** + * Only the exact currently retained revision is readable; no latest fallback. + */ + revision: string; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_OperationObservation { + reference: AgentHistoryCheckpointLoadReceiptOutput_RecoveryRef; + outcome: AgentHistoryCheckpointLoadReceiptOutput_OperationOutcome; + cleanup: AgentHistoryCheckpointLoadReceiptOutput_OperationCleanup; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_RecoveryRef { + source: AgentHistoryCheckpointLoadReceiptOutput_DeploymentRef; + namespace: AgentHistoryCheckpointLoadReceiptOutput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentHistoryCheckpointLoadReceiptOutput_RecoveryContract; + recovery: AgentHistoryCheckpointLoadReceiptOutput_RecoveryProtocol; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentHistoryCheckpointLoadReceiptOutput_RecoveryContract { + name: string; + version: number; + transport: AgentHistoryCheckpointLoadReceiptOutput_Transport; + digest: string; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoad { + head: AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointHead; + inspection: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointInspection; + state: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoadState; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointObject; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointInspection { + sourceSessionId: string; + operationId: string; +} +export interface AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceSnapshot { + identity: string; + consistency: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceWarning; + inventory: AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointObject; +} export interface AgentHistoryCheckpointReceiptInput_RecoveryRef { source: AgentHistoryCheckpointReceiptInput_DeploymentRef; namespace: AgentHistoryCheckpointReceiptInput_InstanceId; @@ -1876,6 +2414,72 @@ export interface AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointObjec digest: string; bytes: number; } +export interface AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentLoadHistoryCheckpointOutput_AgentCheckpointInspection { + sourceSessionId: string; + operationId: string; +} +export interface AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot { + identity: string; + consistency: AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceWarning; + inventory: AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentLoadedHistoryCheckpointArtifactInput_AgentCheckpointInspection { + sourceSessionId: string; + operationId: string; +} +export interface AgentLoadedHistoryCheckpointArtifactOutput_AgentCheckpointArtifactChunk { + artifact: AgentLoadedHistoryCheckpointArtifactOutput_AgentCheckpointArtifact; + offset: number; + /** + * Base64, at most 32 KiB decoded. Empty only at the end. + */ + data: string; +} +export interface AgentLoadedHistoryCheckpointArtifactOutput_AgentCheckpointArtifact { + id: string; + name: string; + mimeType?: string | null; + bytes: number; +} +export interface AgentLoadedHistoryCheckpointArtifactsInput_AgentCheckpointInspection { + sourceSessionId: string; + operationId: string; +} +export interface AgentLoadedHistoryCheckpointArtifactsOutput_AgentCheckpointArtifact { + id: string; + name: string; + mimeType?: string | null; + bytes: number; +} +export interface AgentLoadedHistoryCheckpointHistoryInput_AgentCheckpointInspection { + sourceSessionId: string; + operationId: string; +} +/** + * Disposable presentation locator, never authority or a canonical execution fact. + * The source is the original emitting fact; the index precedes UI filtering. + */ +export interface AgentLoadedHistoryCheckpointHistoryInput_HistoryRowPosition { + sourceSequence: number; + itemIndex: number; +} +/** + * Byte offset in one message field's UTF-8 text, not a bearer capability. + */ +export interface AgentLoadedHistoryCheckpointHistoryInput_Cursor { + field?: AgentLoadedHistoryCheckpointHistoryInput_HistoryField & string; + position: AgentLoadedHistoryCheckpointHistoryInput_HistoryRowPosition; + offset: number; +} export interface AgentPreviewHistoryCheckpointOutput_AgentHistoryCheckpointSession { id: string; revision: number; @@ -1900,6 +2504,29 @@ export interface AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointObjec digest: string; bytes: number; } +export interface AgentPublishSessionCheckpointInput_AgentHistoryCheckpointPublish { + sessionId: string; + expectedRevision: string | null; + /** + * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. + */ + confirmedSubtreeDigest: string; +} +export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointPublication { + state: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState; + /** + * Cleanup failure does not undo a successful publication. + */ + cleanupPending: boolean; +} +export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} export interface AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead { revision: string; manifest: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointObject; @@ -1917,6 +2544,263 @@ export interface AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointSession id: string; revision: number; } +export interface AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot { + identity: string; + consistency: AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceWarning; + inventory: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentRestoreSessionCheckpointInput_AgentHistoryCheckpointRead { + sessionId: string; + /** + * Only the exact currently retained revision is readable; no latest fallback. + */ + revision: string; +} +export interface AgentRestoreSessionCheckpointInput_AgentDirectorySelection { + rootId: string; + segments: string[]; + directoryIdentity: string; +} +export interface AgentRestoreSessionCheckpointInput_AgentWorkspacePermissions { + access: AgentRestoreSessionCheckpointInput_AgentWorkspaceAccess; + requestPermissions: boolean; +} +export interface AgentRestoreSessionCheckpointInput_AgentExecutorSelection { + executor: string; + settings: AgentRestoreSessionCheckpointInput_ExecutorSettings; +} +/** + * Executor-owned model selection, independent of Host model connections. + */ +export interface AgentRestoreSessionCheckpointInput_ExecutorSettings { + model?: string | null; + thinkingLevel?: AgentRestoreSessionCheckpointInput_ExecutorEffort | null; + runtime?: string | null; +} +export interface AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceSnapshot { + identity: string; + consistency: AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceWarning; + inventory: AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointObject; +} +export interface AgentSessionCheckpointReceiptInput_RecoveryRef { + source: AgentSessionCheckpointReceiptInput_DeploymentRef; + namespace: AgentSessionCheckpointReceiptInput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentSessionCheckpointReceiptInput_RecoveryContract; + recovery: AgentSessionCheckpointReceiptInput_RecoveryProtocol; +} +export interface AgentSessionCheckpointReceiptInput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentSessionCheckpointReceiptInput_RecoveryContract { + name: string; + version: number; + transport: AgentSessionCheckpointReceiptInput_Transport; + digest: string; +} +export interface AgentSessionCheckpointReceiptInput_AgentSessionCheckpointPublish { + history: AgentSessionCheckpointReceiptInput_AgentHistoryCheckpointPublish; + /** + * Must be explicitly selected. Maka does not fence other applications. + */ + workspaceConsistency: AgentSessionCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency; +} +export interface AgentSessionCheckpointReceiptInput_AgentHistoryCheckpointPublish { + sessionId: string; + expectedRevision: string | null; + /** + * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. + */ + confirmedSubtreeDigest: string; +} +export interface AgentSessionCheckpointReceiptOutput_OperationObservation { + reference: AgentSessionCheckpointReceiptOutput_RecoveryRef; + outcome: AgentSessionCheckpointReceiptOutput_OperationOutcome; + cleanup: AgentSessionCheckpointReceiptOutput_OperationCleanup; +} +export interface AgentSessionCheckpointReceiptOutput_RecoveryRef { + source: AgentSessionCheckpointReceiptOutput_DeploymentRef; + namespace: AgentSessionCheckpointReceiptOutput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentSessionCheckpointReceiptOutput_RecoveryContract; + recovery: AgentSessionCheckpointReceiptOutput_RecoveryProtocol; +} +export interface AgentSessionCheckpointReceiptOutput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentSessionCheckpointReceiptOutput_RecoveryContract { + name: string; + version: number; + transport: AgentSessionCheckpointReceiptOutput_Transport; + digest: string; +} +export interface AgentSessionCheckpointReceiptOutput_AgentSessionCheckpointPublication { + publication: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointPublication; + workspaceConsistency: AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency; + /** + * Stable warning, present even with a caller attestation. + */ + warning: AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning; +} +export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointPublication { + state: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointState; + /** + * Cleanup failure does not undo a successful publication. + */ + cleanupPending: boolean; +} +export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointObject; +} +export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentSessionCheckpointRestoreReceiptInput_RecoveryRef { + source: AgentSessionCheckpointRestoreReceiptInput_DeploymentRef; + namespace: AgentSessionCheckpointRestoreReceiptInput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentSessionCheckpointRestoreReceiptInput_RecoveryContract; + recovery: AgentSessionCheckpointRestoreReceiptInput_RecoveryProtocol; +} +export interface AgentSessionCheckpointRestoreReceiptInput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentSessionCheckpointRestoreReceiptInput_RecoveryContract { + name: string; + version: number; + transport: AgentSessionCheckpointRestoreReceiptInput_Transport; + digest: string; +} +/** + * Restore files into a new child of a selected directory and rebind the same, + * unchanged local Session. It never starts a model/tool call or rewinds history. + */ +export interface AgentSessionCheckpointRestoreReceiptInput_AgentSessionCheckpointRestore { + checkpoint: AgentSessionCheckpointRestoreReceiptInput_AgentHistoryCheckpointRead; + parent: AgentSessionCheckpointRestoreReceiptInput_AgentDirectorySelection; + permissions: AgentSessionCheckpointRestoreReceiptInput_AgentWorkspacePermissions; + backend: AgentSessionCheckpointRestoreReceiptInput_AgentBackendPreference; + acceptExternalWritersRisk: boolean; +} +export interface AgentSessionCheckpointRestoreReceiptInput_AgentHistoryCheckpointRead { + sessionId: string; + /** + * Only the exact currently retained revision is readable; no latest fallback. + */ + revision: string; +} +export interface AgentSessionCheckpointRestoreReceiptInput_AgentDirectorySelection { + rootId: string; + segments: string[]; + directoryIdentity: string; +} +export interface AgentSessionCheckpointRestoreReceiptInput_AgentWorkspacePermissions { + access: AgentSessionCheckpointRestoreReceiptInput_AgentWorkspaceAccess; + requestPermissions: boolean; +} +export interface AgentSessionCheckpointRestoreReceiptInput_AgentExecutorSelection { + executor: string; + settings: AgentSessionCheckpointRestoreReceiptInput_ExecutorSettings; +} +/** + * Executor-owned model selection, independent of Host model connections. + */ +export interface AgentSessionCheckpointRestoreReceiptInput_ExecutorSettings { + model?: string | null; + thinkingLevel?: AgentSessionCheckpointRestoreReceiptInput_ExecutorEffort | null; + runtime?: string | null; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_OperationObservation { + reference: AgentSessionCheckpointRestoreReceiptOutput_RecoveryRef; + outcome: AgentSessionCheckpointRestoreReceiptOutput_OperationOutcome; + cleanup: AgentSessionCheckpointRestoreReceiptOutput_OperationCleanup; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_RecoveryRef { + source: AgentSessionCheckpointRestoreReceiptOutput_DeploymentRef; + namespace: AgentSessionCheckpointRestoreReceiptOutput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentSessionCheckpointRestoreReceiptOutput_RecoveryContract; + recovery: AgentSessionCheckpointRestoreReceiptOutput_RecoveryProtocol; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentSessionCheckpointRestoreReceiptOutput_RecoveryContract { + name: string; + version: number; + transport: AgentSessionCheckpointRestoreReceiptOutput_Transport; + digest: string; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_AgentSessionCheckpointRestored { + operation: string; + sessionId: string; + sessionRevision: number; + head: AgentSessionCheckpointRestoreReceiptOutput_AgentHistoryCheckpointHead; + workspace: AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceSnapshot; + path: string; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentSessionCheckpointRestoreReceiptOutput_AgentHistoryCheckpointObject; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceSnapshot { + identity: string; + consistency: AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentSessionCheckpointRestoreReceiptOutput_AgentCheckpointWorkspaceWarning; + inventory: AgentSessionCheckpointRestoreReceiptOutput_AgentHistoryCheckpointObject; +} export interface AgentSkillCandidatesOutput_AgentSkillCandidate { reference: string; name: string; diff --git a/platform/event-log/Cargo.toml b/platform/event-log/Cargo.toml index e30bc7c7b3b..5a88da492df 100644 --- a/platform/event-log/Cargo.toml +++ b/platform/event-log/Cargo.toml @@ -24,6 +24,8 @@ publish.workspace = true autotests = false [dependencies] +cap-std.workspace = true +cap-fs-ext.workspace = true hex.workspace = true postcard.workspace = true maka-agent-facts.workspace = true diff --git a/platform/event-log/README.md b/platform/event-log/README.md index d84799e4c33..49981dc2d26 100644 --- a/platform/event-log/README.md +++ b/platform/event-log/README.md @@ -41,12 +41,14 @@ Historical success receipts do not grant access to superseded revisions. This internal facility currently requires Unix file/directory durability; unsupported platforms reject publication before claiming an operation. It has no -automatic trigger, wire endpoint, workspace file snapshot, execution grant or -restore/activation path. The format is native history, not the legacy TS -compatibility bundle. Immutable objects are conservatively retained; only owned +automatic trigger, wire endpoint or cross-Root execution handoff. The native +formats are distinct from the legacy TS compatibility bundle; workspace capture +and same-Root directory recovery are described below. Immutable objects are conservatively retained; only owned temporary files are cleaned. Storage usage is therefore not automatically bounded, and object retention/GC remains separate work. +`HistoryCheckpoints::load_exact` pins an exact current Head and imports its validated native bundle into a private inspection database. The load journal lives beside the source Head; the destination native import receipt reconciles an uncertain import without overwriting live history. Root startup processes pending loads in bounded batches from their original materials. Inspections preserve historical IDs but have no live routes, execution configuration or scheduler. Only bounded history/artifact queries are exposed; the private `EventLog` is never returned. Physical workers retain Root custody until target SQL is closed. Loaded inspections remain readable after Head advances and are conservatively retained; there is no inspection GC yet. + Transcript rows expose separate `position` and `order` addresses. `position` locates canonical source content; `order` places a native assistant message at its actual start even when its end arrives after a tool call. Indexed history paging, navigation and copied history use display order while source reads and fences retain canonical provenance. Native message bodies are reconstructed through invocation/message-specific indexes, not by rescanning all earlier tool output. Stores initialize through the current SQL migrations. Unsupported internal layouts and record formats are rejected. Canonical facts and their acceptance, ownership, recovery and budget indexes share the original transaction. Transcript and Recall read models belong to the disposable observation cache. @@ -87,7 +89,6 @@ A statement failure rolls back the entire group unless it is a domain rejection Saved application inspection uses `RootOwner::open_existing` and `EventLog::for_application_inspection`. It requires existing private coordination state and the original exclusive Root/database leases, reads committed application facts including retained WAL content, and closes without changing stored members or bytes. It performs no migration or derived initialization; missing state fails without repair. Only application queries on the owned connection belong to this opening. CLI application read and default-update preview share it; applying an update opens a fresh ordinary writer and recomputes the change. - Original operation contracts are stored once by digest. Operation requests and receipt references carry that identity, so journal reads, caller history and UI delivery do not repeatedly copy service schemas. Reads verify the exact original request fingerprint; admission still validates the complete contract and resource requirements. The operation journal retains complete admitted requests and returned values without separate request, result or receipt-reference size quotas. Exact retries and reopened readers recover the original outcome and cleanup status. @@ -121,3 +122,12 @@ Integration scenarios share `cargo test -p maka-event-log --test integration`; f [Source](src/lib.rs) · [Cargo manifest](Cargo.toml) Startup recovery reads all unresolved execution authority without a separate item or byte quota; completed history stays excluded. Tool-result payloads and frozen request compositions have no application-level storage size cap. The schema migration preserves existing payloads and composition references. + + +Workspace snapshots use a versioned inventory plus immutable file objects. Their +`caller_confirmed_quiescent` / `unverified` declaration is independent of byte +integrity and never claims external-writer fencing. Same-Root workspace restore +requires unchanged live history, materializes a new directory, and commits the +binding and original receipt atomically without advancing checkpoint Head or +starting execution. See the [Agent API](../../plugins/agent/api/README.md#workspace-publication-and-local-continuation) +for the supported file policy, explicit warning, recovery and scope limits. diff --git a/platform/event-log/README.zh-CN.md b/platform/event-log/README.zh-CN.md index 392c8613cac..7e47459b8b8 100644 --- a/platform/event-log/README.zh-CN.md +++ b/platform/event-log/README.zh-CN.md @@ -38,11 +38,13 @@ Root 直到工作结束。精确读取只支持当前 retained checkpoint revisi 替换成最新版本;历史成功回执也不授予已被替代版本的读取能力。 该内部能力当前要求 Unix 文件及目录持久化语义,其他平台在请求 claim 前 -拒绝发布。它不提供自动触发、协议入口、Workspace 文件快照、执行授权或恢复 -启动;原生历史格式不兼容旧 TS compatibility bundle。不可变对象暂时保守 +拒绝发布。它不提供自动触发、协议入口或跨 Root 执行交接;Workspace 捕获及 +同 Root 目录恢复见下文。原生格式不兼容旧 TS compatibility bundle。不可变对象暂时保守 保留,只清理明确归属的临时文件,因此磁盘使用并非自动有界;retention/GC 属于后续工作。 +`HistoryCheckpoints::load_exact` 固定指定的当前 Head,将校验后的原生 Bundle 导入私有检查数据库。加载日志与源 Head 同分区;目标原生导入回执用于消歧,不覆盖实时历史。Root 启动时分批使用原材料恢复未完成加载。检查副本保留历史 ID,但没有实时路由、执行配置或调度器;仅暴露有界历史和 Artifact 查询,不返回私有 `EventLog`。物理 worker 持有 Root 直到目标 SQL 关闭。已加载副本在 Head 推进后仍可读,保守保留,尚无检查副本 GC。 + 存储通过当前 SQL 迁移初始化;不支持的内部布局和记录格式会被拒绝。规范事实及其接受、所有权、恢复和预算索引共享原事务;transcript 与 Recall 读模型属于可丢弃的 observation 缓存。 管理 SQLite 事件存储和 State Root 租约,持久化应用配置与包、操作回执、会话、轮次及恢复记录。独占所有权见 `root`,已提交的组合见 `applications`,中断恢复见 `recovery`。服务激活前必须先取得 Root 的持久化所有权;业务策略属于插件,规范事实由这里记录。 @@ -81,7 +83,6 @@ Control 中的调用意图、项目登记、workspace attachment、Session 路 已保存应用的检查使用 `RootOwner::open_existing` 和 `EventLog::for_application_inspection`,要求已有的私有协调状态,并持有原有的独占 Root/数据库租约。检查读取已提交的应用事实,包括保留在 WAL 中的内容,关闭后不改变存储成员或字节;不执行迁移或派生初始化,缺失状态直接报错,不做修复。该打开方式仅供原连接上的应用查询使用。CLI 的 application read 与默认更新预览共用此入口;实际应用更新时重新打开普通写入所有者,并重新计算变更。 - 原始操作契约按摘要只存储一次。操作请求和回执引用只携带该身份,日志读取、调用方历史与 UI 传递不再逐次复制服务 schema。读取仍核对原始请求指纹,执行准入仍校验完整契约和资源要求。 操作日志完整保存已受理的请求和返回值,不另设请求、结果或回执引用的大小额度。精确重试和重开后的读取恢复原始结果与清理状态。 @@ -115,3 +116,6 @@ Recall 在 [FTS5](https://sqlite.org/fts5.html#the_detail_option) 中记录每 [源码](src/lib.rs) · [Cargo 配置](Cargo.toml) 启动恢复读取全部未决执行状态,不另设条数或字节配额,已完成历史仍不参与扫描。工具结果 payload 与冻结请求配置不设应用层存储大小上限,数据库迁移保留已有内容和配置引用。 + + +Workspace 存档采用版本化清单和不可变文件对象;`caller_confirmed_quiescent` / `unverified` 声明与字节完整性分开,不能宣称已经隔离外部写入。同 Root 目录恢复要求实时历史未推进,在新目录物化,并原子提交绑定与原回执;不推进 checkpoint Head、不自动执行。文件策略、明确风险提示、故障恢复和范围见 [Agent API](../../plugins/agent/api/README.zh-CN.md)。 diff --git a/platform/event-log/migrations/0018_checkpoint_loads.sql b/platform/event-log/migrations/0018_checkpoint_loads.sql new file mode 100644 index 00000000000..db7dc3ddf45 --- /dev/null +++ b/platform/event-log/migrations/0018_checkpoint_loads.sql @@ -0,0 +1,32 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +CREATE TABLE history_checkpoint_loads ( + load_key TEXT PRIMARY KEY NOT NULL, + request_json TEXT NOT NULL CHECK(json_valid(request_json)), + result_json TEXT NOT NULL CHECK(json_valid(result_json)) +); +CREATE INDEX history_checkpoint_load_recovery ON history_checkpoint_loads(load_key) +WHERE json_extract(result_json,'$.state.kind')='preparing'; + +CREATE TABLE checkpoint_workspace_restores ( + restore_key TEXT PRIMARY KEY NOT NULL, + request_json TEXT NOT NULL CHECK(json_valid(request_json)), + prepared_json TEXT NOT NULL CHECK(json_valid(prepared_json)), + directory_identity_json TEXT CHECK(directory_identity_json IS NULL OR json_valid(directory_identity_json)), + result_json TEXT CHECK(result_json IS NULL OR json_valid(result_json)) +); diff --git a/platform/event-log/src/checkpoints.rs b/platform/event-log/src/checkpoints.rs index 79429f0d0fb..bf9fe25e0d2 100644 --- a/platform/event-log/src/checkpoints.rs +++ b/platform/event-log/src/checkpoints.rs @@ -17,12 +17,19 @@ * under the License. */ -//! Immutable history-only checkpoints. These are not workspace snapshots or -//! execution grants, and their revisions are independent of live Session revisions. +//! Immutable history and Workspace checkpoints. Conditional consistency is not an +//! execution grant; checkpoint revisions are independent of live Session revisions. +mod load; +mod restore; +mod workspace; use crate::{StoreError, bundle::Inventory}; +pub use load::{LoadOperation, LoadState, LoadedCheckpoint}; +pub(crate) use load::{LoadRequest, load_key}; use maka_plugin_core::composition::InstanceId; +pub use restore::{PreparedRestore, RestoreOperation, RestoredSession}; use serde::{Deserialize, Serialize}; use uuid::Uuid; +pub use workspace::{WorkspaceCapture, WorkspaceConsistency, WorkspaceEntry, WorkspaceSnapshot}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -45,6 +52,9 @@ pub struct PublicationRequest { pub expected_revision: Option, /// Confirmation of catalog membership, not a live revision precondition. pub confirmed_subtree_digest: String, + /// Trusted assembly supplies the source; never a model-authored path. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -68,11 +78,26 @@ pub enum Manifest { /// The native format's own framed-stream checksum. transfer_digest: String, }, + #[serde(rename = "native_session_v1")] + NativeSession { + root_id: String, + scope: Scope, + inventory: Inventory, + bundle: ObjectRef, + transfer_digest: String, + workspace: WorkspaceSnapshot, + }, } impl Manifest { pub fn bundle(&self) -> &ObjectRef { match self { - Self::NativeHistory { bundle, .. } => bundle, + Self::NativeHistory { bundle, .. } | Self::NativeSession { bundle, .. } => bundle, + } + } + pub fn workspace(&self) -> Option<&WorkspaceSnapshot> { + match self { + Self::NativeHistory { .. } => None, + Self::NativeSession { workspace, .. } => Some(workspace), } } } diff --git a/platform/event-log/src/checkpoints/load.rs b/platform/event-log/src/checkpoints/load.rs new file mode 100644 index 00000000000..e5101dd6a03 --- /dev/null +++ b/platform/event-log/src/checkpoints/load.rs @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! A loaded checkpoint is isolated history, never a runnable Session. +use super::*; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LoadOperation { + pub operation: Uuid, + pub fingerprint: String, + pub revision: Revision, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum LoadState { + Preparing, + Ready { + session_ids: Vec, + through: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace: Option, + }, + Unavailable, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LoadedCheckpoint { + pub head: Head, + pub state: LoadState, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct LoadRequest { + pub scope: Scope, + pub session_id: String, + pub load: LoadOperation, +} +impl LoadRequest { + pub(crate) fn validate(&self) -> Result<(), StoreError> { + crate::sessions::validate_id(&self.session_id)?; + self.scope + .namespace + .validate() + .map_err(|e| invalid(e.to_string()))?; + if self.scope.deployment.is_nil() + || !valid_digest(&self.scope.state_domain) + || self.load.operation.is_nil() + || self.load.revision.0.is_nil() + || !valid_digest(&self.load.fingerprint) + { + return Err(invalid("invalid checkpoint load request")); + } + Ok(()) + } + pub(crate) fn key(&self) -> Result { + load_key(&self.scope, &self.session_id, self.load.operation) + } + pub(crate) fn binding(&self) -> Result { + Ok(format!("sha256:{}", digest(&serde_json::to_vec(self)?))) + } +} +pub(crate) fn load_key( + scope: &Scope, + session: &str, + operation: Uuid, +) -> Result { + Ok(digest(&serde_json::to_vec(&(scope, session, operation))?)) +} diff --git a/platform/event-log/src/checkpoints/restore.rs b/platform/event-log/src/checkpoints/restore.rs new file mode 100644 index 00000000000..b169216d1aa --- /dev/null +++ b/platform/event-log/src/checkpoints/restore.rs @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Same-Root recovery at the captured, completed Session revision. +use super::*; +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RestoreOperation { + pub operation: Uuid, + pub fingerprint: String, + pub revision: Revision, + pub parent: std::path::PathBuf, + pub parent_identity: maka_agent_facts::execution::DirectoryIdentity, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PreparedRestore { + pub head: Head, + pub source_revision: u64, + pub workspace: WorkspaceSnapshot, + pub path: std::path::PathBuf, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RestoredSession { + pub operation: Uuid, + pub session_id: String, + pub session_revision: u64, + pub prepared: PreparedRestore, +} diff --git a/platform/event-log/src/checkpoints/workspace.rs b/platform/event-log/src/checkpoints/workspace.rs new file mode 100644 index 00000000000..cb0f922a631 --- /dev/null +++ b/platform/event-log/src/checkpoints/workspace.rs @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use super::*; +use std::path::PathBuf; + +/// Neither variant asserts an OS-enforced point-in-time filesystem snapshot. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceConsistency { + CallerConfirmedQuiescent, + Unverified, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceCapture { + pub root: PathBuf, + pub identity: String, + pub consistency: WorkspaceConsistency, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WorkspaceSnapshot { + pub identity: String, + pub consistency: WorkspaceConsistency, + /// Versioned path inventory; file bodies are separately verified immutable objects. + pub inventory: ObjectRef, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum WorkspaceEntry { + Directory { + path: String, + }, + File { + path: String, + object: ObjectRef, + executable: bool, + }, +} diff --git a/platform/event-log/src/root_store.rs b/platform/event-log/src/root_store.rs index 9fe058ef23b..ac886a6950e 100644 --- a/platform/event-log/src/root_store.rs +++ b/platform/event-log/src/root_store.rs @@ -42,7 +42,7 @@ pub use execution::{ pub(crate) use lifecycle::{StageOwner, StageRegistration}; mod applications; mod attachments; -mod checkpoints; +pub(crate) mod checkpoints; mod computer_apps; mod control; pub use checkpoints::{Checkout, HistoryCheckpoints}; @@ -223,6 +223,7 @@ impl RootStore { // observer before recovery can retain a failed SQL job or stage. store.recover_creations().await?; store.recover_checkpoints().await?; + store.recover_checkpoint_loads().await?; // No admission or retirement can observe work abandoned by the // previous owner as live after a process restart. store.recover_host_effects().await?; diff --git a/platform/event-log/src/root_store/checkpoints.rs b/platform/event-log/src/root_store/checkpoints.rs index 7c5b4519a2a..ac1c492d775 100644 --- a/platform/event-log/src/root_store/checkpoints.rs +++ b/platform/event-log/src/root_store/checkpoints.rs @@ -27,7 +27,10 @@ use crate::checkpoints::{ }; use std::future::Future; mod journal; +mod load; mod objects; +pub(crate) mod restore; +mod workspace; use objects::Objects; #[derive(Clone)] @@ -100,6 +103,13 @@ impl RootStore { } impl HistoryCheckpoints { + /// Resolve trusted source binding before consulting a possibly changed live Session. + pub async fn original_request( + &self, + publication: PublicationRequest, + ) -> Result, StoreError> { + journal::original(&self.session.scoped().await?, self.request(publication)?).await + } fn request(&self, publication: PublicationRequest) -> Result { let request = Request { scope: self.scope.clone(), @@ -185,39 +195,55 @@ impl HistoryCheckpoints { log, self.session.gates.clone(), move |log, objects| async move { - let head = journal::read_head(&log, scope.clone(), session.clone()) - .await? - .filter(|head| head.revision == revision) - .ok_or_else(|| invalid("history checkpoint revision unavailable"))?; - let manifest = objects.manifest(&head.manifest)?; - let Manifest::NativeHistory { - root_id, - scope: origin, - inventory, - .. - } = &manifest; - if root_id != objects.root_id() - || origin != &scope - || inventory.root_session_id != session - { - return Err(invalid("history checkpoint origin differs")); - } - let bundle = objects.read(manifest.bundle())?; - // Confirm the retained revision again after expensive verification. - if journal::read_head(&log, scope, session).await?.as_ref() != Some(&head) { - return Err(invalid("history checkpoint revision unavailable")); - } - Ok(Checkout { - head, - manifest, - bundle, - }) + checkout(&log, &objects, scope, session, revision).await }, ) .await } } +async fn checkout( + log: &EventLog, + objects: &Objects, + scope: Scope, + session: String, + revision: Revision, +) -> Result { + let head = journal::read_head(log, scope.clone(), session.clone()) + .await? + .filter(|head| head.revision == revision) + .ok_or_else(|| invalid("history checkpoint revision unavailable"))?; + let manifest = objects.manifest(&head.manifest)?; + let (Manifest::NativeHistory { + root_id, + scope: origin, + inventory, + .. + } + | Manifest::NativeSession { + root_id, + scope: origin, + inventory, + .. + }) = &manifest; + if root_id != objects.root_id() || origin != &scope || inventory.root_session_id != session { + return Err(invalid("history checkpoint origin differs")); + } + let bundle = objects.read(manifest.bundle())?; + if let Some(snapshot) = manifest.workspace() { + workspace::verify(objects, snapshot)?; + } + // Confirm the retained revision again after expensive verification. + if journal::read_head(log, scope, session).await?.as_ref() != Some(&head) { + return Err(invalid("history checkpoint revision unavailable")); + } + Ok(Checkout { + head, + manifest, + bundle, + }) +} + fn bundle_error(error: BundleError) -> StoreError { match error { BundleError::Store(error) => error, @@ -243,24 +269,45 @@ async fn capture( drop(file); let bundle = objects.publish(&objects.staging(request, "bundle")?)?; boundary("payload"); - let manifest = Manifest::NativeHistory { - root_id: objects.root_id().into(), - scope: request.scope.clone(), - inventory: summary.inventory, - bundle, - transfer_digest: summary.digest, + let manifest = if let Some(source) = &request.publication.workspace { + let workspace = workspace::capture(objects, request, source)?; + Manifest::NativeSession { + root_id: objects.root_id().into(), + scope: request.scope.clone(), + inventory: summary.inventory, + bundle, + transfer_digest: summary.digest, + workspace, + } + } else { + Manifest::NativeHistory { + root_id: objects.root_id().into(), + scope: request.scope.clone(), + inventory: summary.inventory, + bundle, + transfer_digest: summary.digest, + } }; validate_bundle(objects, &manifest).await?; objects.publish_manifest(request, &manifest) } async fn validate_bundle(objects: &Objects, manifest: &Manifest) -> Result<(), StoreError> { - let Manifest::NativeHistory { + if let Some(workspace) = manifest.workspace() { + workspace::verify(objects, workspace)?; + } + let (Manifest::NativeHistory { + inventory, + bundle, + transfer_digest, + .. + } + | Manifest::NativeSession { inventory, bundle, transfer_digest, .. - } = manifest; + }) = manifest; let file = tokio::fs::File::from_std(objects.read(bundle)?); let mut staged = StagedBundle::read(file).await.map_err(bundle_error)?; let result = async { @@ -283,12 +330,18 @@ async fn verify( reference: &ObjectRef, ) -> Result<(), StoreError> { let manifest = objects.manifest(reference)?; - let Manifest::NativeHistory { + let (Manifest::NativeHistory { + root_id, + scope, + inventory, + .. + } + | Manifest::NativeSession { root_id, scope, inventory, .. - } = &manifest; + }) = &manifest; if root_id != objects.root_id() || scope != &request.scope || inventory.root_session_id != request.session_id diff --git a/platform/event-log/src/root_store/checkpoints/journal.rs b/platform/event-log/src/root_store/checkpoints/journal.rs index 1cad7c5a877..25529740e8b 100644 --- a/platform/event-log/src/root_store/checkpoints/journal.rs +++ b/platform/event-log/src/root_store/checkpoints/journal.rs @@ -90,6 +90,51 @@ pub(super) async fn read( .await } +pub(super) async fn original( + log: &EventLog, + request: Request, +) -> Result, StoreError> { + log.connection + .read(move |db| { + Box::pin(async move { + binding(db, &request).await?; + let original: Option = sqlx::query_scalar( + "SELECT request_json FROM history_checkpoint_publications WHERE request_key=?", + ) + .bind(request.key()?) + .fetch_optional(db) + .await?; + original + .map(|json| { + let original: Request = serde_json::from_str(&json)?; + if original.scope != request.scope + || original.session_id != request.session_id + || original.publication.fingerprint != request.publication.fingerprint + || original.publication.expected_revision + != request.publication.expected_revision + || original.publication.confirmed_subtree_digest + != request.publication.confirmed_subtree_digest + || original + .publication + .workspace + .as_ref() + .map(|v| v.consistency) + != request + .publication + .workspace + .as_ref() + .map(|v| v.consistency) + { + return Err(invalid("checkpoint operation inputs differ")); + } + Ok(original.publication) + }) + .transpose() + }) + }) + .await +} + pub(super) async fn read_head( log: &EventLog, scope: Scope, diff --git a/platform/event-log/src/root_store/checkpoints/load.rs b/platform/event-log/src/root_store/checkpoints/load.rs new file mode 100644 index 00000000000..66e6e0bbe4c --- /dev/null +++ b/platform/event-log/src/root_store/checkpoints/load.rs @@ -0,0 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Checkpoint inspections never enter the live Root's Session routes or recovery queues. +use super::*; +use crate::checkpoints::{LoadOperation, LoadRequest, LoadState, LoadedCheckpoint, load_key}; +use maka_agent_ports::session::history::{Page, Position}; +use sqlx::Connection; +use std::collections::BTreeMap; +use uuid::Uuid; + +impl HistoryCheckpoints { + fn load_request(&self, load: LoadOperation) -> Result { + let request = LoadRequest { + scope: self.scope.clone(), + session_id: self.session.id().into(), + load, + }; + request.validate()?; + Ok(request) + } + + pub async fn load_exact(&self, load: LoadOperation) -> Result { + let request = self.load_request(load)?; + let gates = self.session.gates.clone(); + let log = self.session.scoped().await?; + worker(log, gates.clone(), move |log, objects| async move { + let _pin = gates + .write([gates::Key::CheckpointLoad(request.key()?)]) + .await?; + if let Some((prior, loaded)) = read(&log, request.key()?).await? { + if prior != request { + return Err(invalid("checkpoint load identity conflict")); + } + return match loaded.state { + LoadState::Preparing => resume(&log, &objects, &request, loaded).await, + _ => Ok(loaded), + }; + } + let selected = checkout( + &log, + &objects, + request.scope.clone(), + request.session_id.clone(), + request.load.revision, + ) + .await?; + // Durable request and pinned material precede any destination writes. + // Recheck Head at the claim's linearization point. + let loaded = claim(&log, request.clone(), selected.head).await?; + boundary("load_prepared"); + resume(&log, &objects, &request, loaded).await + }) + .await + } + + pub async fn load_receipt( + &self, + load: LoadOperation, + ) -> Result, StoreError> { + let request = self.load_request(load)?; + read(&self.session.scoped().await?, request.key()?) + .await? + .map(|(prior, value)| { + if prior != request { + return Err(invalid("checkpoint load identity conflict")); + } + Ok(value) + }) + .transpose() + } + + pub async fn loaded_history( + &self, + operation: Uuid, + session: String, + position: Position, + thinking: bool, + ) -> Result { + self.with_loaded(operation, session.clone(), move |log, through| async move { + maka_agent_ports::session::history::Read { + session_id: session.clone(), + through: Some(through), + position, + include_thinking: thinking, + } + .validate() + .map_err(|error| invalid(error.to_string()))?; + log.history_text( + &session, + through, + position, + thinking, + crate::transcript::HistoryLimits { + max_bytes: 32 * 1024, + max_chunks: Some(8), + max_json_bytes: 64 * 1024, + }, + ) + .await + }) + .await + } + + pub async fn loaded_artifacts( + &self, + operation: Uuid, + session: String, + offset: u64, + ) -> Result { + self.with_loaded(operation, session.clone(), move |log, _| async move { + log.list_artifacts(&session, offset, 32).await + }) + .await + } + + pub async fn loaded_artifact( + &self, + operation: Uuid, + session: String, + artifact: String, + offset: u64, + ) -> Result< + Option<( + maka_agent_facts::artifact::Artifact, + crate::artifacts::ArtifactChunk, + )>, + StoreError, + > { + self.with_loaded(operation, session.clone(), move |log, _| async move { + let Some(record) = log.get_artifact(&session, &artifact).await?.record else { + return Ok(None); + }; + let chunk = log + .read_artifact_chunk(&session, &artifact, offset, 32 * 1024) + .await? + .ok_or_else(|| invalid("inspection artifact payload missing"))?; + Ok(Some((record, chunk))) + }) + .await + } + + async fn with_loaded( + &self, + operation: Uuid, + session: String, + action: F, + ) -> Result + where + T: Send + 'static, + F: FnOnce(Arc, u64) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, + { + let key = load_key(&self.scope, self.session.id(), operation)?; + let log = self.session.scoped().await?; + let gates = self.session.gates.clone(); + worker(log, gates.clone(), move |log, objects| async move { + let _pin = gates + .write([gates::Key::CheckpointLoad(key.clone())]) + .await?; + let (request, loaded) = read(&log, key) + .await? + .ok_or_else(|| invalid("checkpoint inspection missing"))?; + let LoadState::Ready { + ref session_ids, + through, + .. + } = loaded.state + else { + return Err(invalid("checkpoint inspection is not ready")); + }; + if !session_ids.contains(&session) { + return Err(StoreError::SessionNotFound); + } + let target = destination(&objects, &request, false).await?; + let result = async { + verify_receipt(&target, &objects, &request, &loaded).await?; + action(target.clone(), through).await + } + .await; + close(target, result).await + }) + .await + } +} + +impl RootStore { + pub(in crate::root_store) async fn recover_checkpoint_loads(&self) -> Result<(), StoreError> { + for partition in &self.partitions { + let log = partition.scoped(partition.clone()); + let pending = log.connection.read(|db| Box::pin(async move { + Ok(sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM history_checkpoint_loads WHERE json_extract(result_json,'$.state.kind')='preparing')").fetch_one(db).await?) + })).await?; + if !pending { + continue; + } + worker(log, self.gates.clone(), move |log, objects| async move { + let mut after = String::new(); + loop { + let cursor = after.clone(); + let pending: Vec = log.connection.read(move |db| Box::pin(async move { + Ok(sqlx::query_scalar("SELECT load_key FROM history_checkpoint_loads WHERE json_extract(result_json,'$.state.kind')='preparing' AND load_key>? ORDER BY load_key LIMIT 32").bind(cursor).fetch_all(db).await?) + })).await?; + if pending.is_empty() { + break; + } + for key in pending { + after = key.clone(); + let (request, loaded) = read(&log, key) + .await? + .ok_or_else(|| invalid("checkpoint load disappeared"))?; + if let Err(error) = resume(&log, &objects, &request, loaded.clone()).await { + // Missing/corrupt material does not authorize recapture or block normal startup. + // SQL/IO uncertainty keeps Preparing for a later original-material retry. + if matches!( + error, + StoreError::InvalidTransition(_) | StoreError::SessionConflict + ) { + write( + &log, + &request, + LoadedCheckpoint { + state: LoadState::Unavailable, + ..loaded + }, + ) + .await?; + } else { + eprintln!("Checkpoint inspection recovery remains pending: {error}"); + } + } + } + } + Ok(()) + }) + .await?; + } + Ok(()) + } +} + +async fn read( + log: &EventLog, + key: String, +) -> Result, StoreError> { + log.connection.read(move |db| Box::pin(async move { + let row: Option<(String, String)> = sqlx::query_as("SELECT request_json,result_json FROM history_checkpoint_loads WHERE load_key=?").bind(key).fetch_optional(db).await?; + row.map(|(request, result)| Ok((serde_json::from_str(&request)?, serde_json::from_str(&result)?))).transpose() + })).await +} +async fn claim( + log: &EventLog, + request: LoadRequest, + head: Head, +) -> Result { + log.connection + .run(move |db| { + Box::pin(async move { + let mut tx = db.begin_with("BEGIN IMMEDIATE").await?; + let current: Option = sqlx::query_scalar( + "SELECT head_json FROM history_checkpoint_heads WHERE session_id=?", + ) + .bind(&request.session_id) + .fetch_optional(&mut *tx) + .await?; + if current + .map(|v| serde_json::from_str::(&v)) + .transpose()? + .as_ref() + != Some(&head) + { + return Err(invalid("history checkpoint revision unavailable")); + } + let value = LoadedCheckpoint { + head, + state: LoadState::Preparing, + }; + sqlx::query("INSERT INTO history_checkpoint_loads VALUES(?,?,?)") + .bind(request.key()?) + .bind(serde_json::to_string(&request)?) + .bind(serde_json::to_string(&value)?) + .execute(&mut *tx) + .await?; + tx.commit().await.map_err(StoreError::CommitUnknown)?; + Ok(value) + }) + }) + .await +} +async fn write( + log: &EventLog, + request: &LoadRequest, + value: LoadedCheckpoint, +) -> Result { + let key = request.key()?; + log.connection + .run(move |db| { + Box::pin(async move { + let result = sqlx::query( + "UPDATE history_checkpoint_loads SET result_json=? WHERE load_key=?", + ) + .bind(serde_json::to_string(&value)?) + .bind(key) + .execute(db) + .await?; + if result.rows_affected() != 1 { + return Err(invalid("checkpoint load receipt missing")); + } + Ok(value) + }) + }) + .await +} + +async fn destination( + objects: &Objects, + request: &LoadRequest, + create: bool, +) -> Result, StoreError> { + // Only this private module can open the inspection database. No live routes, + // execution configuration, recovery scheduler or EventLog handle are exported. + let path = objects.inspection(&request.key()?, create)?; + Ok(Arc::new( + EventLog::open_owned(&path, Some(objects.owner.clone()), OpenPurpose::Execution).await?, + )) +} +async fn close(target: Arc, result: Result) -> Result { + if let Err(error) = target.shutdown().await { + // Keep the physical worker, Root custody and per-inspection gate until SQL ends. + target.drain().await; + return Err(error); + } + result +} +fn manifest( + objects: &Objects, + request: &LoadRequest, + loaded: &LoadedCheckpoint, +) -> Result { + let value = objects.manifest(&loaded.head.manifest)?; + let (Manifest::NativeHistory { + root_id, + scope, + inventory, + .. + } + | Manifest::NativeSession { + root_id, + scope, + inventory, + .. + }) = &value; + if loaded.head.revision != request.load.revision + || root_id != objects.root_id() + || scope != &request.scope + || inventory.root_session_id != request.session_id + { + return Err(invalid("checkpoint inspection origin differs")); + } + Ok(value) +} +async fn verify_receipt( + target: &EventLog, + objects: &Objects, + request: &LoadRequest, + loaded: &LoadedCheckpoint, +) -> Result<(), StoreError> { + let (Manifest::NativeHistory { + transfer_digest, + inventory, + .. + } + | Manifest::NativeSession { + transfer_digest, + inventory, + .. + }) = manifest(objects, request, loaded)?; + let receipt = target + .bundle_import_receipt(&transfer_digest, &request.binding()?) + .await? + .ok_or_else(|| invalid("checkpoint inspection import receipt missing"))?; + if receipt.root_session_id != inventory.root_session_id + || receipt.session_ids + != inventory + .sessions + .iter() + .map(|s| s.id.clone()) + .collect::>() + { + return Err(invalid("checkpoint inspection inventory differs")); + } + Ok(()) +} +async fn resume( + log: &EventLog, + objects: &Objects, + request: &LoadRequest, + loaded: LoadedCheckpoint, +) -> Result { + let source = manifest(objects, request, &loaded)?; + let workspace = source.workspace().cloned(); + if let Some(snapshot) = &workspace { + super::workspace::verify(objects, snapshot)?; + objects.inspection_workspace(&request.key()?, snapshot)?; + } + let (Manifest::NativeHistory { + inventory, + transfer_digest, + .. + } + | Manifest::NativeSession { + inventory, + transfer_digest, + .. + }) = &source; + let target = destination(objects, request, true).await?; + let result = async { + if target.bundle_import_receipt(transfer_digest, &request.binding()?).await?.is_none() { + // An unclaimed nonempty database is never adopted or overwritten. + let nonempty = target.connection.read(|db| Box::pin(async move { + Ok(sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM session_control) OR EXISTS(SELECT 1 FROM event_log)").fetch_one(db).await?) + })).await?; + if nonempty { + return Err(invalid("checkpoint inspection destination is occupied")); + } + let file = objects.read(source.bundle())?; + let mut staged = StagedBundle::read(tokio::fs::File::from_std(file)) + .await.map_err(bundle_error)?; + let checked = async { + staged.validate_history().await.map_err(bundle_error)?; + if staged.summary().inventory != *inventory + || staged.summary().digest != *transfer_digest + { + return Err(invalid("checkpoint inspection bundle differs from manifest")); + } + Ok(()) + }.await; + if let Err(error) = checked { + staged.close().await.map_err(bundle_error)?; + return Err(error); + } + let configurations = inventory.sessions.iter() + .map(|session| (session.id.clone(), serde_json::json!({"kind":"checkpoint_inspection"}))) + .collect::>(); + target.import_bundle(staged, &request.binding()?, configurations) + .await.map_err(bundle_error)?; + } + verify_receipt(&target, objects, request, &loaded).await?; + Ok(LoadState::Ready { + session_ids: inventory.sessions.iter().map(|s| s.id.clone()).collect(), + through: target.committed_sequence().await?, + workspace, + }) + }.await; + let state = close(target, result).await?; + boundary("load_imported"); + let value = write(log, request, LoadedCheckpoint { state, ..loaded }).await?; + boundary("load_recorded"); + Ok(value) +} diff --git a/platform/event-log/src/root_store/checkpoints/objects.rs b/platform/event-log/src/root_store/checkpoints/objects.rs index 96c6a7a1b5c..73e61ba265c 100644 --- a/platform/event-log/src/root_store/checkpoints/objects.rs +++ b/platform/event-log/src/root_store/checkpoints/objects.rs @@ -27,7 +27,7 @@ use std::{ /// Runs only on the checkpoint's dedicated physical worker, never a Tokio executor thread. pub(super) struct Objects { - owner: Arc, + pub(super) owner: Arc, directory: PathBuf, } impl Objects { @@ -127,7 +127,7 @@ impl Objects { Ok(serde_json::from_reader(file)?) } pub(super) fn cleanup(&self, request: &Request) -> Result<(), StoreError> { - for kind in ["bundle", "manifest"] { + for kind in ["bundle", "manifest", "workspace", "workspace-file"] { let path = self.staging(request, kind)?; match fs::symlink_metadata(&path) { Ok(metadata) if metadata.file_type().is_file() => fs::remove_file(path)?, @@ -143,9 +143,116 @@ impl Objects { File::open(self.directory.join("staging"))?.sync_all()?; Ok(()) } + pub(super) fn inspection(&self, key: &str, create: bool) -> Result { + self.validate()?; + if !crate::checkpoints::valid_digest(key) { + return Err(invalid("invalid inspection key")); + } + let directory = self.directory.join("inspections"); + if create { + root::private_directory(&directory)?; + } + if !fs::symlink_metadata(&directory)?.file_type().is_dir() { + return Err(invalid("inspection directory is not a directory")); + } + let path = directory.join(format!("{key}.sqlite")); + match fs::symlink_metadata(&path) { + Ok(metadata) if metadata.file_type().is_file() => {} + Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => { + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600).custom_flags(libc::O_NOFOLLOW); + } + let file = options.open(&path)?; + file.sync_all()?; + File::open(&directory)?.sync_all()?; + File::open(&self.directory)?.sync_all()?; + } + Err(error) => return Err(error.into()), + _ => return Err(invalid("inspection database is not a regular file")), + } + Ok(path) + } pub(super) fn root_id(&self) -> &str { self.owner.root_id() } + + pub(super) fn inspection_workspace( + &self, + key: &str, + snapshot: &crate::checkpoints::WorkspaceSnapshot, + ) -> Result { + self.validate()?; + if !crate::checkpoints::valid_digest(key) { + return Err(invalid("invalid inspection key")); + } + let base = self.directory.join("inspections"); + root::private_directory(&base)?; + let marker = base.join(format!("{key}.workspace-owner")); + let bytes = serde_json::to_vec(&(self.root_id(), key, snapshot))?; + // Publish the small ownership marker atomically; a killed write must not + // leave a truncated marker that poisons recovery of the original load. + let preparing = base.join(format!("{key}.workspace-owner-preparing")); + match fs::symlink_metadata(&preparing) { + Ok(m) if m.is_file() => fs::remove_file(&preparing)?, + Ok(_) => { + return Err(invalid( + "checkpoint workspace marker staging is not regular", + )); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.into()), + } + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600).custom_flags(libc::O_NOFOLLOW); + } + let mut file = options.open(&preparing)?; + file.write_all(&bytes)?; + file.sync_all()?; + drop(file); + match fs::hard_link(&preparing, &marker) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + let mut file = open_regular(&marker)?; + let mut actual = Vec::new(); + Read::by_ref(&mut file) + .take(bytes.len() as u64 + 1) + .read_to_end(&mut actual)?; + if actual != bytes { + return Err(invalid("checkpoint workspace owner differs")); + } + } + Err(e) => return Err(e.into()), + } + fs::remove_file(preparing)?; + File::open(&base)?.sync_all()?; + let target = base.join(format!("{key}.workspace")); + if target.try_exists()? { + if !fs::symlink_metadata(&target)?.is_dir() { + return Err(invalid("checkpoint workspace is not a directory")); + } + return Ok(target); + } + let staging = base.join(format!("{key}.workspace-preparing")); + match fs::symlink_metadata(&staging) { + Ok(m) if m.is_dir() => fs::remove_dir_all(&staging)?, + Ok(_) => return Err(invalid("checkpoint workspace staging is not owned")), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.into()), + } + root::private_directory(&staging)?; + super::workspace::materialize(self, snapshot, &staging)?; + fs::rename(&staging, &target)?; + File::open(&base)?.sync_all()?; + Ok(target) + } } fn open_regular(path: &Path) -> Result { let mut options = OpenOptions::new(); diff --git a/platform/event-log/src/root_store/checkpoints/restore.rs b/platform/event-log/src/root_store/checkpoints/restore.rs new file mode 100644 index 00000000000..86361f9e0a6 --- /dev/null +++ b/platform/event-log/src/root_store/checkpoints/restore.rs @@ -0,0 +1,415 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Same-Root workspace recovery; the live history must still equal the captured cut. +use super::*; +use crate::checkpoints::{PreparedRestore, RestoreOperation, RestoredSession}; +use maka_agent_facts::execution::DirectoryIdentity; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sqlx::SqliteConnection; + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +struct RequestIdentity { + scope: Scope, + session: String, + operation: RestoreOperation, +} +impl RequestIdentity { + fn key(&self) -> Result { + if !crate::checkpoints::valid_digest(&self.operation.fingerprint) + || !self.operation.parent.is_absolute() + { + return Err(invalid("invalid checkpoint restore request")); + } + crate::checkpoints::load_key(&self.scope, &self.session, self.operation.operation) + } +} +struct Preparation { + prepared: PreparedRestore, + directory_identity: Option, + result: Option, +} +impl Preparation { + fn directory_identity(&self) -> Result<&DirectoryIdentity, StoreError> { + self.directory_identity + .as_ref() + .ok_or_else(|| invalid("checkpoint restore directory preparation incomplete")) + } +} +async fn prior( + log: &EventLog, + request: &RequestIdentity, +) -> Result, StoreError> { + let key = request.key()?; + let expected = serde_json::to_string(request)?; + log.connection.read(move |db| Box::pin(async move { + let row: Option<(String, String, Option, Option)> = sqlx::query_as("SELECT request_json,prepared_json,directory_identity_json,result_json FROM checkpoint_workspace_restores WHERE restore_key=?").bind(key).fetch_optional(db).await?; + row.map(|(original, prepared, directory_identity, result)| { + if original != expected { return Err(invalid("checkpoint restore identity conflict")); } + Ok(Preparation { + prepared: serde_json::from_str(&prepared)?, + directory_identity: directory_identity.map(|v|serde_json::from_str(&v)).transpose()?, + result: result.map(|r|serde_json::from_str(&r)).transpose()?, + }) + }).transpose() + })).await +} + +pub(crate) struct Commit { + key: String, + request: RequestIdentity, + prepared: PreparedRestore, + directory_identity: DirectoryIdentity, +} +impl Commit { + pub(crate) async fn validate(&self, db: &mut SqliteConnection) -> Result<(), StoreError> { + let current: Option = + sqlx::query_scalar("SELECT head_json FROM history_checkpoint_heads WHERE session_id=?") + .bind(&self.request.session) + .fetch_optional(&mut *db) + .await?; + if current + .as_deref() + .map(serde_json::from_str::) + .transpose()? + .as_ref() + != Some(&self.prepared.head) + { + return Err(invalid("checkpoint restore head moved")); + } + let row: Option<(String, String, Option, Option)> = sqlx::query_as("SELECT request_json,prepared_json,directory_identity_json,result_json FROM checkpoint_workspace_restores WHERE restore_key=?").bind(&self.key).fetch_optional(db).await?; + if row + != Some(( + serde_json::to_string(&self.request)?, + serde_json::to_string(&self.prepared)?, + Some(serde_json::to_string(&self.directory_identity)?), + None, + )) + { + return Err(invalid("checkpoint restore claim changed")); + } + Ok(()) + } + pub(crate) async fn record( + self, + db: &mut SqliteConnection, + revision: u64, + ) -> Result<(), StoreError> { + let result = RestoredSession { + operation: self.request.operation.operation, + session_id: self.request.session, + session_revision: revision, + prepared: self.prepared, + }; + sqlx::query("UPDATE checkpoint_workspace_restores SET result_json=? WHERE restore_key=? AND result_json IS NULL").bind(serde_json::to_string(&result)?).bind(self.key).execute(db).await?; + Ok(()) + } +} +impl HistoryCheckpoints { + fn restore_identity(&self, operation: RestoreOperation) -> RequestIdentity { + RequestIdentity { + scope: self.scope.clone(), + session: self.session.id().into(), + operation, + } + } + pub async fn workspace_restore_receipt( + &self, + operation: uuid::Uuid, + fingerprint: String, + revision: Revision, + ) -> Result, StoreError> { + let key = crate::checkpoints::load_key(&self.scope, self.session.id(), operation)?; + let scope = self.scope.clone(); + let session = self.session.id().to_owned(); + self.session.scoped().await?.connection.read(move |db|Box::pin(async move { + let row: Option<(String,Option)> = sqlx::query_as("SELECT request_json,result_json FROM checkpoint_workspace_restores WHERE restore_key=?").bind(key).fetch_optional(db).await?; + row.map(|(request,result)| { + let request: RequestIdentity = serde_json::from_str(&request)?; + if request.scope != scope || request.session != session || request.operation.operation != operation || request.operation.fingerprint != fingerprint || request.operation.revision != revision { return Err(invalid("checkpoint restore identity conflict")); } + result.map(|r|serde_json::from_str(&r).map_err(StoreError::from)).transpose() + }).transpose().map(Option::flatten) + })).await + } + /// The parent was selected by the trusted Host. Always creates a new child, + /// never overwrites an existing project. Partial destinations fail closed. + pub async fn prepare_workspace_restore( + &self, + operation: RestoreOperation, + ) -> Result { + let request = self.restore_identity(operation); + request.key()?; + let log = self.session.exclusive_metadata(true).await?; + worker(log, self.session.gates.clone(), move |log, objects| async move { + if let Some(prior) = prior(&log, &request).await? { + if prior.result.is_none() { verify_destination(&objects, &prior.prepared, &request.operation, prior.directory_identity()?)?; } + return Ok(prior.prepared); + } + let checkout = checkout(&log, &objects, request.scope.clone(), request.session.clone(), request.operation.revision).await?; + let Manifest::NativeSession { inventory, workspace, .. } = checkout.manifest else { return Err(invalid("checkpoint has no workspace")); }; + if inventory.sessions.len() != 1 || inventory.sessions[0].id != request.session { return Err(invalid("checkpoint restore requires one ordinary Session")); } + let current = log.get_session::(&request.session).await?.ok_or(StoreError::SessionNotFound)?; + if current.archived || current.revision != inventory.sessions[0].revision { return Err(invalid("live Session advanced beyond checkpoint")); } + let parent = request.operation.parent.canonicalize()?; + let directory = cap_std::fs::Dir::open_ambient_dir(&parent, cap_std::ambient_authority())?; + if directory_identity(&directory)? != request.operation.parent_identity { return Err(invalid("restore parent directory changed")); } + if parent.starts_with(objects.owner.canonical_path()) { return Err(invalid("restore destination overlaps private Root")); } + let prepared = PreparedRestore { head: checkout.head, source_revision: current.revision, workspace, path: parent.join(format!("maka-restored-{}", request.key()?)) }; + let key = request.key()?; + let encoded = serde_json::to_string(&request)?; + let data = serde_json::to_string(&prepared)?; + // The durable claim precedes writes, and commit rechecks both Head and live revision. + log.connection.run(move |db|Box::pin(async move { + sqlx::query("INSERT INTO checkpoint_workspace_restores(restore_key,request_json,prepared_json) VALUES(?,?,?)").bind(key).bind(encoded).bind(data).execute(db).await?; Ok(()) + })).await?; + let name = prepared.path.file_name().ok_or_else(||invalid("restore destination missing"))?; + directory.create_dir(name)?; + use cap_fs_ext::DirExt; + let destination = directory.open_dir_nofollow(name)?; + let identity = directory_identity(&destination)?; + workspace::materialize_in(&objects, &prepared.workspace, &destination)?; + directory.into_std_file().sync_all()?; + verify_destination(&objects, &prepared, &request.operation, &identity)?; + let key = request.key()?; + let identity = serde_json::to_string(&identity)?; + // A claim alone never proves we created the directory. Publish its + // physical identity only after the complete copy and directory sync. + log.connection.run(move |db|Box::pin(async move { + let changed = sqlx::query("UPDATE checkpoint_workspace_restores SET directory_identity_json=? WHERE restore_key=? AND directory_identity_json IS NULL AND result_json IS NULL").bind(identity).bind(key).execute(db).await?; + if changed.rows_affected() != 1 { return Err(invalid("checkpoint restore claim changed")); } + Ok(()) + })).await?; + Ok(prepared) + }).await + } + /// Configuration and the original restore receipt commit atomically. This + /// rebinding never rewinds history or starts an invocation. + pub async fn commit_workspace_restore( + &self, + operation: RestoreOperation, + configuration: Value, + now: u64, + ) -> Result { + crate::sessions::validate_time(now)?; + let request = self.restore_identity(operation); + let log = self.session.exclusive_metadata(true).await?; + let gates = self.session.gates.clone(); + let control = self + .session + .control + .upgrade() + .ok_or(StoreError::ConnectionClosed)?; + worker(log, gates.clone(), move |log, objects| async move { + let preparation = prior(&log, &request) + .await? + .ok_or_else(|| invalid("checkpoint restore not prepared"))?; + if let Some(result) = preparation.result { + return Ok(result); + } + let directory_identity = preparation.directory_identity()?.clone(); + let prepared = preparation.prepared; + let reference = crate::workspace_bindings::attached(&configuration)? + .ok_or_else(|| invalid("restore workspace binding missing"))?; + let pin = gates.read([gates::Key::Binding(reference.id)]).await?; + verify_destination(&objects, &prepared, &request.operation, &directory_identity)?; + let binding = control + .workspace_binding::(reference.id) + .await? + .ok_or_else(|| invalid("restore binding missing"))?; + if binding.workspace.get("path").and_then(Value::as_str) != prepared.path.to_str() { + return Err(invalid("restore binding differs from prepared workspace")); + } + let key = request.key()?; + let result = log + .mutate_session_metadata::( + &request.session, + prepared.source_revision, + true, + Some(now), + Some(control), + Some(Commit { + key, + request: request.clone(), + prepared: prepared.clone(), + directory_identity, + }), + move |value| { + *value = configuration; + Ok(()) + }, + ) + .await?; + drop(pin); + if !matches!(result, crate::sessions::SessionMutation::Committed(_)) { + return Err(invalid("live Session advanced beyond checkpoint")); + } + prior(&log, &request) + .await? + .and_then(|v| v.result) + .ok_or_else(|| invalid("checkpoint restore receipt missing")) + }) + .await + } +} +fn verify_destination( + objects: &Objects, + prepared: &PreparedRestore, + operation: &RestoreOperation, + identity: &DirectoryIdentity, +) -> Result<(), StoreError> { + use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt}; + use cap_std::fs::{Dir, OpenOptions}; + use sha2::{Digest, Sha256}; + use std::io::Read; + let parent = Dir::open_ambient_dir(&operation.parent, cap_std::ambient_authority())?; + if directory_identity(&parent)? != operation.parent_identity { + return Err(invalid("restore parent directory changed")); + } + let root = parent.open_dir_nofollow( + prepared + .path + .file_name() + .ok_or_else(|| invalid("restore destination missing"))?, + )?; + if &directory_identity(&root)? != identity { + return Err(invalid("restore destination directory changed")); + } + let entries = workspace::inventory(objects, &prepared.workspace)?; + let expected: std::collections::BTreeSet<_> = entries + .iter() + .map(|entry| match entry { + crate::checkpoints::WorkspaceEntry::Directory { path } + | crate::checkpoints::WorkspaceEntry::File { path, .. } => path.clone(), + }) + .collect(); + fn paths( + dir: &Dir, + prefix: &str, + result: &mut std::collections::BTreeSet, + ) -> Result<(), StoreError> { + for entry in dir.entries()? { + let entry = entry?; + let name = entry + .file_name() + .into_string() + .map_err(|_| invalid("restored path invalid"))?; + if prefix.is_empty() && name == ".maka-workspace.json" { + continue; + } + let path = if prefix.is_empty() { + name.clone() + } else { + format!("{prefix}/{name}") + }; + if result.len() >= 20_000 || path.split('/').count() > 65 { + return Err(invalid("restored workspace bounds exceeded")); + } + result.insert(path.clone()); + if entry.file_type()?.is_dir() { + paths(&dir.open_dir_nofollow(&name)?, &path, result)?; + } + } + Ok(()) + } + let mut actual = std::collections::BTreeSet::new(); + paths(&root, "", &mut actual)?; + if actual != expected { + return Err(invalid("restored workspace inventory changed")); + } + for entry in entries { + let path = match &entry { + crate::checkpoints::WorkspaceEntry::Directory { path } + | crate::checkpoints::WorkspaceEntry::File { path, .. } => path, + }; + let mut parts = path.split('/').collect::>(); + let name = parts.pop().unwrap(); + let mut parent = root.try_clone()?; + for part in parts { + parent = parent.open_dir_nofollow(part)?; + } + match &entry { + crate::checkpoints::WorkspaceEntry::Directory { .. } => { + parent.open_dir_nofollow(name)?; + } + crate::checkpoints::WorkspaceEntry::File { + object, executable, .. + } => { + let mut options = OpenOptions::new(); + options.read(true).follow(FollowSymlinks::No); + #[cfg(unix)] + { + use cap_std::fs::OpenOptionsExt; + options.custom_flags(libc::O_NONBLOCK); + } + let mut file = parent.open_with(name, &options)?; + if !file.metadata()?.is_file() { + return Err(invalid("restored workspace entry changed")); + } + #[cfg(unix)] + { + use cap_std::fs::MetadataExt; + if (file.metadata()?.mode() & 0o111 != 0) != *executable { + return Err(invalid("restored executable bit changed")); + } + } + #[cfg(not(unix))] + let _ = executable; + let mut hash = Sha256::new(); + let mut bytes = 0u64; + let mut buffer = [0u8; 65536]; + loop { + let count = file.read(&mut buffer)?; + if count == 0 { + break; + } + bytes += count as u64; + if bytes > object.bytes { + return Err(invalid("restored workspace changed")); + } + hash.update(&buffer[..count]); + } + if bytes != object.bytes || hex::encode(hash.finalize()) != object.digest { + return Err(invalid("restored workspace changed")); + } + } + } + } + Ok(()) +} + +pub(super) fn directory_identity( + directory: &cap_std::fs::Dir, +) -> Result { + use cap_fs_ext::MetadataExt; + let metadata = directory.dir_metadata()?; + let birth = metadata + .created() + .ok() + .and_then(|time| time.into_std().duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| (d.as_secs(), d.subsec_nanos())); + maka_agent_facts::execution::DirectoryIdentity::try_from( + maka_agent_facts::artifact::content_digest(&serde_json::to_vec(&( + metadata.dev(), + metadata.ino(), + birth, + ))?), + ) + .map_err(invalid) +} diff --git a/platform/event-log/src/root_store/checkpoints/tests.rs b/platform/event-log/src/root_store/checkpoints/tests.rs index 40d94ec1f5f..4ca7021c833 100644 --- a/platform/event-log/src/root_store/checkpoints/tests.rs +++ b/platform/event-log/src/root_store/checkpoints/tests.rs @@ -26,6 +26,8 @@ use std::{ use uuid::Uuid; mod body; +mod load; +mod workspace; fn namespaces(path: &Path) -> root::RootNamespaces { root::RootNamespaces { @@ -60,6 +62,7 @@ async fn fixture(path: &Path) -> (RootStore, HistoryCheckpoints, PublicationRequ fingerprint: "a".repeat(64), expected_revision: None, confirmed_subtree_digest: store.preview_bundle("source").await.unwrap().subtree_digest, + workspace: None, }; (store, handle, request) } @@ -710,6 +713,7 @@ async fn killed_writer_recovers_without_a_new_capture() { fingerprint: "a".repeat(64), expected_revision: None, confirmed_subtree_digest: store.preview_bundle("source").await.unwrap().subtree_digest, + workspace: None, }; let receipt = handle.receipt(input.clone()).await.unwrap().unwrap(); if matches!(phase, "claimed" | "payload") { diff --git a/platform/event-log/src/root_store/checkpoints/tests/body.rs b/platform/event-log/src/root_store/checkpoints/tests/body.rs index d741f934eb0..9237a26484d 100644 --- a/platform/event-log/src/root_store/checkpoints/tests/body.rs +++ b/platform/event-log/src/root_store/checkpoints/tests/body.rs @@ -42,8 +42,71 @@ async fn checkpoint_keeps_tool_bytes_after_source_collection_and_import() { let plain = |fact| EventWrite::plain(RuntimeEvent::new(invocation.clone(), fact)).unwrap(); log.append(&plain(Fact::InvocationOpened { configuration: None, - input: InvocationInput::Code { - source: "fixture".into(), + input: InvocationInput::Message { + content: "read the large tool result".into(), + source_messages: vec![], + request_fingerprint: None, + }, + })) + .await + .unwrap(); + use maka_agent_facts::model::{ + ModelFinishReason, ModelPart, ModelToolCall, ModelUsage, + recorded::{CompletionPart, ModelCompletion, ModelPartRecord}, + }; + let context = log + .read_model_view("source", Some(&invocation.invocation_id)) + .await + .unwrap(); + log.append(&plain(Fact::ModelRequested { + source_capture: maka_agent_facts::event::SourceCapture { + publication_watermark: context.selection.captured_through, + prior_unknown: context.selection.prior_unknown, + }, + context_breakdown: None, + item_acceptance: false, + purpose: maka_agent_facts::context::ModelPurpose::Main, + context: None, + checkpoint_event_id: None, + step_id: "step".into(), + model_id: "model".into(), + source_scope: context.source_evidence.scope, + source_high_water: context.source_evidence.high_water, + source_digest: context.source_evidence.digest, + effective_source_digest: Some(context.effective_source_digest), + input_digest: "fixture".into(), + route_identity: "route".into(), + })) + .await + .unwrap(); + log.append(&plain(Fact::ModelCompleted { + step_id: "step".into(), + output: ModelCompletion { + compaction: None, + parts: vec![CompletionPart::Own(ModelPartRecord { + ordinal: 0, + message_id: maka_agent_facts::tool_call::tool_use_id( + &invocation.invocation_id, + "step:call", + ), + started_at: std::time::UNIX_EPOCH, + provider_part_id: None, + part: Arc::new(ModelPart::ToolCall { + call: ModelToolCall { + id: "call".into(), + name: "Read".into(), + input: json!({}).into(), + provider_options: None, + provider_executed: false, + }, + }), + })], + finish_reason: ModelFinishReason::ToolCalls, + usage: ModelUsage::default(), + provider_options: None, + response_id: None, + model: None, + timestamp: None, }, })) .await @@ -51,8 +114,8 @@ async fn checkpoint_keeps_tool_bytes_after_source_collection_and_import() { log.append(&plain(Fact::ToolDispatched { presentation: None, title: None, - operation_id: "read".into(), - call: ToolCallIdentity::standalone("call".into()), + operation_id: "step:call".into(), + call: ToolCallIdentity::provider("step".into(), "call".into()), name: "Read".into(), input: json!({}), })) @@ -63,7 +126,7 @@ async fn checkpoint_keeps_tool_bytes_after_source_collection_and_import() { "result".into(), std::time::UNIX_EPOCH, invocation.clone(), - "read".into(), + "step:call".into(), ToolSuccess::projected( ToolOutput::Text(text.clone()), DurableToolProjection::Text { text: text.clone() }, @@ -130,6 +193,36 @@ async fn checkpoint_keeps_tool_bytes_after_source_collection_and_import() { let store = RootStore::open(owner(directory.path())).await.unwrap(); let handle = store.history_checkpoints("source", scope()).await.unwrap(); assert_eq!(handle.receipt(request).await.unwrap(), Some(value)); + // The public inspection path must page tool bodies after canonical source GC. + let load = crate::checkpoints::LoadOperation { + operation: Uuid::new_v4(), + fingerprint: "c".repeat(64), + revision: head.revision, + }; + handle.load_exact(load.clone()).await.unwrap(); + use maka_agent_ports::session::history::{Page, Position, Role}; + let mut position = Position::Start; + let mut restored_tool = String::new(); + let mut pages = 0; + loop { + let Page::Ready { chunks, after, .. } = handle + .loaded_history(load.operation, "source".into(), position, false) + .await + .unwrap(); + pages += 1; + for chunk in chunks { + if chunk.role == Role::ToolResult { + restored_tool.push_str(&chunk.text); + } + } + let Some(cursor) = after else { + break; + }; + position = Position::After { cursor }; + assert!(pages < 100, "paging must advance"); + } + assert!(pages > 1); + assert_eq!(restored_tool, text); let checkout = handle.checkout_exact(head.revision).await.unwrap(); let mut staged = StagedBundle::read(tokio::fs::File::from_std(checkout.bundle)) .await diff --git a/platform/event-log/src/root_store/checkpoints/tests/load.rs b/platform/event-log/src/root_store/checkpoints/tests/load.rs new file mode 100644 index 00000000000..c21f5428dee --- /dev/null +++ b/platform/event-log/src/root_store/checkpoints/tests/load.rs @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use super::*; +use crate::checkpoints::{LoadOperation, LoadRequest, LoadState}; +use maka_agent_ports::session::history::Position; + +fn request(revision: Revision) -> LoadOperation { + LoadOperation { + operation: Uuid::from_u128(101), + fingerprint: "b".repeat(64), + revision, + } +} +#[tokio::test] +async fn exact_load_is_idempotent_isolated_and_survives_head_move() { + let dir = tempfile::tempdir().unwrap(); + let (store, handle, publish) = fixture(dir.path()).await; + let head = published(&handle.publish(publish.clone()).await.unwrap()); + let input = request(head.revision); + let (a, b) = tokio::join!( + handle.load_exact(input.clone()), + handle.load_exact(input.clone()) + ); + let value = a.unwrap(); + assert_eq!(b.unwrap(), value); + assert!(matches!(value.state, LoadState::Ready { .. })); + let source = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + assert_eq!(source.configuration["title"], "original"); + assert_eq!(source.revision, 1); + let mut next = publish; + next.operation = Uuid::from_u128(102); + next.expected_revision = Some(head.revision); + let newer = published(&handle.publish(next).await.unwrap()); + let mut conflict = input.clone(); + conflict.revision = newer.revision; + assert!( + handle + .load_exact(conflict) + .await + .unwrap_err() + .to_string() + .contains("identity conflict") + ); + let mut stale = input.clone(); + stale.operation = Uuid::from_u128(103); + assert!(handle.load_exact(stale.clone()).await.is_err()); + assert!(handle.load_receipt(stale).await.unwrap().is_none()); + assert_eq!(handle.load_exact(input.clone()).await.unwrap(), value); + store.close().await.unwrap(); + let store = RootStore::open(owner(dir.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert_eq!( + handle.load_receipt(input.clone()).await.unwrap(), + Some(value) + ); + handle + .loaded_history(input.operation, "source".into(), Position::Tail, false) + .await + .unwrap(); + assert!(matches!( + handle + .loaded_history(input.operation, "foreign".into(), Position::Tail, false) + .await, + Err(StoreError::SessionNotFound) + )); + let mut foreign = scope(); + foreign.deployment = Uuid::new_v4(); + let other = store.history_checkpoints("source", foreign).await.unwrap(); + assert!( + other + .loaded_history(input.operation, "source".into(), Position::Tail, false) + .await + .is_err() + ); + store.close().await.unwrap(); + assert!(handle.load_receipt(input).await.is_err()); +} + +#[tokio::test] +async fn load_reconciles_committed_import_when_receipt_write_failed() { + let dir = tempfile::tempdir().unwrap(); + let (store, handle, publish) = fixture(dir.path()).await; + let head = published(&handle.publish(publish.clone()).await.unwrap()); + let input = request(head.revision); + let log = handle.session.scoped().await.unwrap(); + log.connection.run(|db|Box::pin(async move { + sqlx::raw_sql("CREATE TRIGGER fail_load_receipt BEFORE UPDATE ON history_checkpoint_loads BEGIN SELECT RAISE(ABORT,'lost receipt'); END;").execute(db).await?; Ok(()) + })).await.unwrap(); + assert!( + handle + .load_exact(input.clone()) + .await + .unwrap_err() + .to_string() + .contains("lost receipt") + ); + assert_eq!( + handle + .load_receipt(input.clone()) + .await + .unwrap() + .unwrap() + .state, + LoadState::Preparing + ); + log.connection + .run(|db| { + Box::pin(async move { + sqlx::query("DROP TRIGGER fail_load_receipt") + .execute(db) + .await?; + Ok(()) + }) + }) + .await + .unwrap(); + drop(log); + let mut newer = publish; + newer.operation = Uuid::new_v4(); + newer.expected_revision = Some(head.revision); + handle.publish(newer).await.unwrap(); + store.close().await.unwrap(); + let store = RootStore::open(owner(dir.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + let result = handle.load_receipt(input.clone()).await.unwrap().unwrap(); + assert_eq!(result.head, head); + assert!(matches!(result.state, LoadState::Ready { .. })); + assert_eq!(handle.load_exact(input).await.unwrap(), result); + store.close().await.unwrap(); +} + +#[tokio::test] +async fn load_rejects_corruption_and_never_adopts_occupied_destination() { + let dir = tempfile::tempdir().unwrap(); + let (store, handle, publish) = fixture(dir.path()).await; + let head = published(&handle.publish(publish).await.unwrap()); + let input = request(head.revision); + let record = LoadRequest { + scope: scope(), + session_id: "source".into(), + load: input.clone(), + }; + let objects = Objects::open(store.owned_root().unwrap()).unwrap(); + let path = objects.inspection(&record.key().unwrap(), true).unwrap(); + let occupied = EventLog::open(&path).await.unwrap(); + occupied + .create_session("intruder", "fixture", &json!({}), 1) + .await + .unwrap(); + occupied.close().await.unwrap(); + assert!( + handle + .load_exact(input.clone()) + .await + .unwrap_err() + .to_string() + .contains("occupied") + ); + let occupied = EventLog::open(&path).await.unwrap(); + assert!( + occupied + .get_session::("intruder") + .await + .unwrap() + .is_some() + ); + assert!( + occupied + .get_session::("source") + .await + .unwrap() + .is_none() + ); + occupied.close().await.unwrap(); + let mut corrupt = input.clone(); + corrupt.operation = Uuid::new_v4(); + let manifest = objects.manifest(&head.manifest).unwrap(); + std::fs::write( + dir.path() + .join("root/history-checkpoints/objects") + .join(&manifest.bundle().digest), + b"corrupt", + ) + .unwrap(); + assert!(handle.load_exact(corrupt.clone()).await.is_err()); + assert!(handle.load_receipt(corrupt).await.unwrap().is_none()); + drop(objects); + store.close().await.unwrap(); +} + +#[test] +fn load_crash_child() { + let Some(path) = std::env::var_os("MAKA_LOAD_TEST_ROOT") else { + return; + }; + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async { + let (store, handle, publish) = fixture(Path::new(&path)).await; + let head = published(&handle.publish(publish).await.unwrap()); + handle.load_exact(request(head.revision)).await.unwrap(); + store.close().await.unwrap(); + }); +} + +#[tokio::test] +async fn load_process_death_recovers_original_target_at_each_publication_boundary() { + for phase in ["load_prepared", "load_imported", "load_recorded"] { + let dir = tempfile::tempdir().unwrap(); + let ready = dir.path().join("ready"); + let mut child = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "root_store::checkpoints::tests::load::load_crash_child", + "--nocapture", + ]) + .env("MAKA_LOAD_TEST_ROOT", dir.path()) + .env("MAKA_CHECKPOINT_TEST_PHASE", phase) + .env("MAKA_CHECKPOINT_TEST_READY", &ready) + .stdout(std::process::Stdio::null()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(30); + while !ready.exists() { + if let Some(exit) = child.try_wait().unwrap() { + panic!("load child exited: {exit}"); + } + if Instant::now() > deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("load child timeout {phase}"); + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + child.kill().unwrap(); + child.wait().unwrap(); + let store = RootStore::open(owner(dir.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + let head = handle.read_head().await.unwrap().unwrap(); + let input = request(head.revision); + let receipt = handle.load_receipt(input.clone()).await.unwrap().unwrap(); + assert!( + matches!(receipt.state, LoadState::Ready { .. }), + "{phase}: {receipt:?}" + ); + assert_eq!(handle.load_exact(input).await.unwrap(), receipt); + store.close().await.unwrap(); + } +} diff --git a/platform/event-log/src/root_store/checkpoints/tests/workspace.rs b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs new file mode 100644 index 00000000000..7d0372f3d2a --- /dev/null +++ b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use super::*; +use crate::checkpoints::{ + LoadOperation, LoadRequest, LoadState, WorkspaceCapture, WorkspaceConsistency, WorkspaceEntry, +}; + +#[tokio::test] +async fn workspace_material_is_immutable_and_load_preserves_conditional_guarantee() { + for mode in [ + WorkspaceConsistency::CallerConfirmedQuiescent, + WorkspaceConsistency::Unverified, + ] { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + std::fs::create_dir_all(source.join("empty")).unwrap(); + std::fs::create_dir_all(source.join("src")).unwrap(); + std::fs::create_dir_all(source.join(".git")).unwrap(); + std::fs::write(source.join("src/main.rs"), "original").unwrap(); + std::fs::write(source.join(".git/config"), "not archived").unwrap(); + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions( + source.join("src/main.rs"), + std::fs::Permissions::from_mode(0o755), + ) + .unwrap(); + let (store, handle, mut request) = fixture(dir.path()).await; + request.workspace = Some(WorkspaceCapture { + root: source.clone(), + identity: "workspace-id".into(), + consistency: mode, + }); + let head = published(&handle.publish(request.clone()).await.unwrap()); + std::fs::write(source.join("src/main.rs"), "later version").unwrap(); + let checkout = handle.checkout_exact(head.revision).await.unwrap(); + assert_eq!(checkout.manifest.workspace().unwrap().consistency, mode); + let operation = LoadOperation { + operation: Uuid::new_v4(), + fingerprint: "b".repeat(64), + revision: head.revision, + }; + let loaded = handle.load_exact(operation.clone()).await.unwrap(); + let LoadState::Ready { + workspace: Some(snapshot), + .. + } = loaded.state + else { + panic!("workspace absent") + }; + assert_eq!(snapshot.consistency, mode); + let key = LoadRequest { + scope: scope(), + session_id: "source".into(), + load: operation.clone(), + } + .key() + .unwrap(); + let path = store + .owned_root() + .unwrap() + .canonical_path() + .join("history-checkpoints/inspections") + .join(format!("{key}.workspace")); + assert_eq!( + std::fs::read_to_string(path.join("src/main.rs")).unwrap(), + "original" + ); + assert!(path.join("empty").is_dir()); + assert!(!path.join(".git").exists()); + assert_ne!( + std::fs::metadata(path.join("src/main.rs")) + .unwrap() + .permissions() + .mode() + & 0o111, + 0 + ); + assert_eq!(published(&handle.publish(request).await.unwrap()), head); + store.close().await.unwrap(); + let store = RootStore::open(owner(dir.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert!(matches!( + handle.load_receipt(operation).await.unwrap().unwrap().state, + LoadState::Ready { + workspace: Some(_), + .. + } + )); + store.close().await.unwrap(); + } +} + +#[tokio::test] +async fn unsafe_workspace_material_cannot_publish_a_head() { + for kind in ["secret", "symlink", "fifo", "overlap"] { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + std::fs::create_dir(&source).unwrap(); + let (store, handle, mut request) = fixture(dir.path()).await; + match kind { + "secret" => std::fs::write(source.join(".env"), "secret").unwrap(), + "symlink" => std::os::unix::fs::symlink(dir.path(), source.join("link")).unwrap(), + "fifo" => { + assert!( + std::process::Command::new("mkfifo") + .arg(source.join("pipe")) + .status() + .unwrap() + .success() + ); + } + _ => {} + } + request.workspace = Some(WorkspaceCapture { + root: if kind == "overlap" { + dir.path().to_owned() + } else { + source + }, + identity: "workspace-id".into(), + consistency: WorkspaceConsistency::Unverified, + }); + assert!( + tokio::time::timeout(Duration::from_secs(10), handle.publish(request)) + .await + .unwrap() + .is_err() + ); + assert!(handle.read_head().await.unwrap().is_none()); + store.close().await.unwrap(); + } +} + +#[tokio::test] +async fn corrupt_workspace_object_prevents_exact_load() { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + std::fs::create_dir(&source).unwrap(); + std::fs::write(source.join("file"), "original").unwrap(); + let (store, handle, mut request) = fixture(dir.path()).await; + request.workspace = Some(WorkspaceCapture { + root: source, + identity: "workspace-id".into(), + consistency: WorkspaceConsistency::Unverified, + }); + let head = published(&handle.publish(request).await.unwrap()); + let checkout = handle.checkout_exact(head.revision).await.unwrap(); + let objects = Objects::open(store.owned_root().unwrap()).unwrap(); + let inventory: serde_json::Value = serde_json::from_reader( + objects + .read(&checkout.manifest.workspace().unwrap().inventory) + .unwrap(), + ) + .unwrap(); + let entries: Vec = + serde_json::from_value(inventory["entries"].clone()).unwrap(); + let WorkspaceEntry::File { object, .. } = &entries[0] else { + panic!() + }; + std::fs::write( + store + .owned_root() + .unwrap() + .canonical_path() + .join("history-checkpoints/objects") + .join(&object.digest), + "corrupt", + ) + .unwrap(); + let request = LoadOperation { + operation: Uuid::new_v4(), + fingerprint: "b".repeat(64), + revision: head.revision, + }; + assert!(handle.load_exact(request.clone()).await.is_err()); + assert!(handle.load_receipt(request).await.unwrap().is_none()); + store.close().await.unwrap(); +} + +#[tokio::test] +async fn detected_external_writer_aborts_capture_even_when_caller_confirmed() { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + std::fs::create_dir(&source).unwrap(); + std::fs::write(source.join("file"), "before").unwrap(); + let (store, handle, publication) = fixture(dir.path()).await; + let objects = Objects::open(store.owned_root().unwrap()).unwrap(); + let changed = source.join("file"); + super::super::workspace::AFTER_SCAN.with(|hook| { + *hook.borrow_mut() = Some(Box::new(move || std::fs::write(changed, "after!").unwrap())) + }); + let request = handle.request(publication).unwrap(); + let error = super::super::workspace::capture( + &objects, + &request, + &WorkspaceCapture { + root: source, + identity: "workspace".into(), + consistency: WorkspaceConsistency::CallerConfirmedQuiescent, + }, + ) + .unwrap_err(); + assert!(error.to_string().contains("changed")); + assert!(handle.read_head().await.unwrap().is_none()); + store.close().await.unwrap(); +} + +async fn restore_fixture( + path: &Path, +) -> ( + RootStore, + HistoryCheckpoints, + PublicationRequest, + crate::checkpoints::RestoreOperation, + crate::checkpoints::PreparedRestore, + serde_json::Value, +) { + let (store, handle, mut publish) = fixture(path).await; + let source = path.join("project"); + std::fs::create_dir(&source).unwrap(); + std::fs::write(source.join("file"), "frozen").unwrap(); + let binding = store + .attach_workspace("fixture", &json!({"path":source})) + .await + .unwrap(); + store + .update_session_metadata("source", 1, move |value: &mut serde_json::Value| { + value["workspace"] = serde_json::to_value(binding)?; + value["boundary_revision"] = json!(1); + Ok(()) + }) + .await + .unwrap(); + publish.confirmed_subtree_digest = store.preview_bundle("source").await.unwrap().subtree_digest; + publish.workspace = Some(WorkspaceCapture { + root: source, + identity: "workspace".into(), + consistency: WorkspaceConsistency::Unverified, + }); + let head = published(&handle.publish(publish.clone()).await.unwrap()); + let parent = path.join("destinations"); + std::fs::create_dir(&parent).unwrap(); + let directory = + cap_std::fs::Dir::open_ambient_dir(&parent, cap_std::ambient_authority()).unwrap(); + let operation = crate::checkpoints::RestoreOperation { + operation: Uuid::new_v4(), + fingerprint: "d".repeat(64), + revision: head.revision, + parent, + parent_identity: super::super::restore::directory_identity(&directory).unwrap(), + }; + let prepared = handle + .prepare_workspace_restore(operation.clone()) + .await + .unwrap(); + let binding = store + .attach_workspace("fixture", &json!({"path":prepared.path})) + .await + .unwrap(); + let configuration = json!({"title":"restored","workspace":binding,"boundary_revision":2}); + (store, handle, publish, operation, prepared, configuration) +} +#[tokio::test] +async fn restore_receipt_is_atomic_with_binding_and_survives_later_source_changes() { + let directory = tempfile::tempdir().unwrap(); + let (store, _handle, _, operation, prepared, configuration) = + restore_fixture(directory.path()).await; + store.close().await.unwrap(); + let store = RootStore::open(owner(directory.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert_eq!( + handle + .prepare_workspace_restore(operation.clone()) + .await + .unwrap(), + prepared + ); + let before = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + let log = handle.session.scoped().await.unwrap(); + log.connection.run(|db|Box::pin(async move {sqlx::raw_sql("CREATE TRIGGER reject_restore BEFORE UPDATE ON checkpoint_workspace_restores BEGIN SELECT RAISE(ABORT,'reject receipt'); END;").execute(db).await?;Ok(())})).await.unwrap(); + drop(log); + assert!( + handle + .commit_workspace_restore(operation.clone(), configuration.clone(), 2) + .await + .is_err() + ); + let unchanged = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + assert_eq!(unchanged.revision, before.revision); + assert_eq!(unchanged.configuration, before.configuration); + let log = handle.session.scoped().await.unwrap(); + log.connection + .run(|db| { + Box::pin(async move { + sqlx::query("DROP TRIGGER reject_restore") + .execute(db) + .await?; + Ok(()) + }) + }) + .await + .unwrap(); + drop(log); + let committed = handle + .commit_workspace_restore(operation.clone(), configuration.clone(), 3) + .await + .unwrap(); + assert_eq!(committed.prepared, prepared); + assert_eq!( + handle + .commit_workspace_restore(operation.clone(), configuration, 3) + .await + .unwrap(), + committed + ); + store + .update_session_metadata( + "source", + committed.session_revision, + |value: &mut serde_json::Value| { + value["title"] = json!("later"); + Ok(()) + }, + ) + .await + .unwrap(); + std::fs::rename(&operation.parent, directory.path().join("moved-target")).unwrap(); + assert_eq!( + handle + .workspace_restore_receipt( + operation.operation, + operation.fingerprint.clone(), + operation.revision + ) + .await + .unwrap(), + Some(committed.clone()) + ); + store.close().await.unwrap(); + let store = RootStore::open(owner(directory.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert_eq!( + handle + .workspace_restore_receipt( + operation.operation, + operation.fingerprint, + operation.revision + ) + .await + .unwrap(), + Some(committed) + ); + store.close().await.unwrap(); +} +#[tokio::test] +async fn restore_rejects_head_race_and_changed_destination_without_rebinding() { + for race in ["head", "files", "extra", "parent", "symlink", "replacement"] { + let directory = tempfile::tempdir().unwrap(); + let (store, handle, mut publish, operation, prepared, configuration) = + restore_fixture(directory.path()).await; + match race { + "head" => { + publish.operation = Uuid::new_v4(); + publish.expected_revision = Some(operation.revision); + handle.publish(publish).await.unwrap(); + } + "files" => std::fs::write(prepared.path.join("file"), "modified").unwrap(), + "parent" => { + std::fs::rename(&operation.parent, directory.path().join("old-parent")).unwrap(); + std::fs::create_dir(&operation.parent).unwrap(); + } + "symlink" => { + let moved = operation.parent.join("moved-workspace"); + std::fs::rename(&prepared.path, &moved).unwrap(); + std::os::unix::fs::symlink(moved, &prepared.path).unwrap(); + } + "replacement" => { + std::fs::rename(&prepared.path, operation.parent.join("original-output")).unwrap(); + std::fs::create_dir(&prepared.path).unwrap(); + std::fs::write(prepared.path.join("file"), "frozen").unwrap(); + } + _ => std::fs::write(prepared.path.join("extra"), "unrecorded").unwrap(), + } + assert!( + handle + .commit_workspace_restore(operation.clone(), configuration, 2) + .await + .is_err(), + "accepted {race} race" + ); + assert_eq!( + store + .get_session::("source") + .await + .unwrap() + .unwrap() + .revision, + prepared.source_revision + ); + assert!( + handle + .workspace_restore_receipt( + operation.operation, + operation.fingerprint, + operation.revision + ) + .await + .unwrap() + .is_none() + ); + store.close().await.unwrap(); + } +} + +#[tokio::test] +async fn restore_retry_never_adopts_an_occupied_destination() { + let directory = tempfile::tempdir().unwrap(); + let (store, handle, _, mut operation, _, _) = restore_fixture(directory.path()).await; + operation.operation = Uuid::new_v4(); + let key = crate::checkpoints::load_key(&scope(), "source", operation.operation).unwrap(); + let occupied = operation.parent.join(format!("maka-restored-{key}")); + std::fs::create_dir(&occupied).unwrap(); + std::fs::write(occupied.join("file"), "frozen").unwrap(); + assert!( + handle + .prepare_workspace_restore(operation.clone()) + .await + .is_err() + ); + store.close().await.unwrap(); + let store = RootStore::open(owner(directory.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert!( + handle.prepare_workspace_restore(operation).await.is_err(), + "retry adopted a directory it did not create" + ); + assert_eq!( + std::fs::read_to_string(occupied.join("file")).unwrap(), + "frozen" + ); + store.close().await.unwrap(); +} + +#[tokio::test] +async fn restore_cannot_commit_a_copy_without_a_durable_completion_record() { + let directory = tempfile::tempdir().unwrap(); + let (store, handle, _, mut operation, _, _) = restore_fixture(directory.path()).await; + operation.operation = Uuid::new_v4(); + let log = handle.session.scoped().await.unwrap(); + log.connection.run(|db|Box::pin(async move { + sqlx::raw_sql("CREATE TRIGGER reject_preparation BEFORE UPDATE OF directory_identity_json ON checkpoint_workspace_restores BEGIN SELECT RAISE(ABORT,'copy completion not recorded'); END;").execute(db).await?; + Ok(()) + })).await.unwrap(); + drop(log); + assert!( + handle + .prepare_workspace_restore(operation.clone()) + .await + .is_err() + ); + let key = crate::checkpoints::load_key(&scope(), "source", operation.operation).unwrap(); + let path = operation.parent.join(format!("maka-restored-{key}")); + assert_eq!( + std::fs::read_to_string(path.join("file")).unwrap(), + "frozen" + ); + let binding = store + .attach_workspace("fixture", &json!({"path":path})) + .await + .unwrap(); + let configuration = json!({"workspace":binding,"boundary_revision":2}); + assert!( + handle + .commit_workspace_restore(operation, configuration, 2) + .await + .is_err() + ); + store.close().await.unwrap(); +} diff --git a/platform/event-log/src/root_store/checkpoints/workspace.rs b/platform/event-log/src/root_store/checkpoints/workspace.rs new file mode 100644 index 00000000000..0964d3a4b33 --- /dev/null +++ b/platform/event-log/src/root_store/checkpoints/workspace.rs @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Bounded directory capture. Caller attestations never become filesystem fencing. +use super::*; +use crate::checkpoints::{WorkspaceCapture, WorkspaceEntry, WorkspaceSnapshot}; +use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt}; +use cap_std::fs::{Dir, OpenOptions}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, BTreeSet}, + io::{Read, Write}, + path::Path, +}; + +#[cfg(test)] +thread_local! { pub(super) static AFTER_SCAN: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; } + +const MAX_ENTRIES: usize = 20_000; +const MAX_BYTES: u64 = 2 * 1024 * 1024 * 1024; +const MAX_INVENTORY: u64 = 16 * 1024 * 1024; + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Inventory { + version: u32, + entries: Vec, +} + +fn relative(path: &str) -> Result<(), StoreError> { + if path.is_empty() + || path.len() > 4096 + || path.contains('\\') + || path.contains(':') + || path.chars().any(char::is_control) + || path + .split('/') + .any(|p| p.is_empty() || p == "." || p == ".." || p.ends_with(['.', ' '])) + { + return Err(invalid("nonportable checkpoint workspace path")); + } + Ok(()) +} + +fn excluded(name: &str) -> bool { + matches!( + name, + ".git" | "node_modules" | ".DS_Store" | "__pycache__" | ".cache" | ".maka-workspace.json" + ) +} + +fn secret(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + name == ".env" + || name.starts_with(".env.") + && !matches!( + name.as_str(), + ".env.example" | ".env.sample" | ".env.template" + ) + || matches!( + name.as_str(), + ".ssh" | ".aws" | ".gnupg" | "credentials.json" | "id_rsa" | "id_ed25519" + ) + || name.ends_with(".key") + || name.ends_with(".pem") + || name.ends_with(".p12") +} + +fn stamp(meta: &cap_std::fs::Metadata) -> Result { + #[cfg(unix)] + { + use cap_std::fs::MetadataExt; + Ok(format!( + "{}:{}:{}:{}:{}:{}:{:?}", + meta.dev(), + meta.ino(), + meta.len(), + meta.mode(), + meta.ctime(), + meta.ctime_nsec(), + meta.modified()? + )) + } + #[cfg(not(unix))] + { + Ok(format!( + "{}:{:?}:{:?}", + meta.len(), + meta.file_type(), + meta.modified()? + )) + } +} + +fn scan( + dir: &Dir, + prefix: &str, + output: &mut BTreeMap, + depth: usize, +) -> Result<(), StoreError> { + if depth > 64 { + return Err(invalid("checkpoint workspace depth limit")); + } + for entry in dir.entries()? { + let entry = entry?; + let name = entry + .file_name() + .into_string() + .map_err(|_| invalid("non-UTF8 checkpoint workspace path"))?; + if excluded(&name) { + continue; + } + if secret(&name) { + return Err(invalid( + "checkpoint workspace contains an excluded secret path", + )); + } + let path = if prefix.is_empty() { + name.clone() + } else { + format!("{prefix}/{name}") + }; + relative(&path)?; + if output.len() >= MAX_ENTRIES { + return Err(invalid("checkpoint workspace entry limit")); + } + let metadata = dir.symlink_metadata(&name)?; + if !metadata.is_dir() && !metadata.is_file() { + return Err(invalid( + "checkpoint workspace links and special files are unsupported", + )); + } + output.insert(path.clone(), stamp(&metadata)?); + if metadata.is_dir() { + scan(&dir.open_dir_nofollow(&name)?, &path, output, depth + 1)?; + } + } + Ok(()) +} + +fn parent(dir: &Dir, path: &str) -> Result<(Dir, String), StoreError> { + relative(path)?; + let mut parts = path.split('/').collect::>(); + let name = parts.pop().unwrap().to_owned(); + let mut current = dir.try_clone()?; + for part in parts { + current = current.open_dir_nofollow(part)?; + } + Ok((current, name)) +} + +pub(super) fn capture( + objects: &Objects, + request: &Request, + source: &WorkspaceCapture, +) -> Result { + if !source.root.is_absolute() || source.identity.is_empty() { + return Err(invalid("invalid checkpoint workspace binding")); + } + let source_path = std::fs::canonicalize(&source.root)?; + let root_path = objects.owner.canonical_path(); + if source_path.starts_with(root_path) || root_path.starts_with(&source_path) { + return Err(invalid("checkpoint workspace overlaps state root")); + } + let dir = Dir::open_ambient_dir(&source_path, cap_std::ambient_authority())?; + let before_root = stamp(&dir.dir_metadata()?)?; + let mut before = BTreeMap::new(); + scan(&dir, "", &mut before, 0)?; + #[cfg(test)] + AFTER_SCAN.with(|hook| { + if let Some(action) = hook.borrow_mut().take() { + action(); + } + }); + let mut entries = Vec::new(); + let mut total = 0u64; + let start = std::time::Instant::now(); + for (path, expected) in &before { + if start.elapsed() > std::time::Duration::from_secs(120) { + return Err(invalid("checkpoint workspace capture deadline")); + } + objects.owner.validate_current()?; + let (parent, name) = parent(&dir, path)?; + let metadata = parent.symlink_metadata(&name)?; + if stamp(&metadata)? != *expected { + return Err(invalid("checkpoint workspace changed during capture")); + } + if metadata.is_dir() { + entries.push(WorkspaceEntry::Directory { path: path.clone() }); + continue; + } + let mut options = OpenOptions::new(); + options.read(true).follow(FollowSymlinks::No); + #[cfg(unix)] + { + use cap_std::fs::OpenOptionsExt; + options.custom_flags(libc::O_NONBLOCK); + } + let mut file = parent.open_with(&name, &options)?; + if !file.metadata()?.is_file() || stamp(&file.metadata()?)? != *expected { + return Err(invalid("checkpoint workspace file changed")); + } + let mut staged = objects.create(request, "workspace-file")?; + let mut copied = 0u64; + let mut buffer = [0u8; 65536]; + loop { + if start.elapsed() > std::time::Duration::from_secs(120) { + return Err(invalid("checkpoint workspace capture deadline")); + } + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + copied += n as u64; + total += n as u64; + if total > MAX_BYTES || copied > metadata.len() { + return Err(invalid("checkpoint workspace size limit or source change")); + } + staged.write_all(&buffer[..n])?; + } + if copied != metadata.len() + || stamp(&file.metadata()?)? != *expected + || stamp(&parent.symlink_metadata(&name)?)? != *expected + { + return Err(invalid("checkpoint workspace changed during capture")); + } + staged.sync_all()?; + drop(staged); + let staging = objects.staging(request, "workspace-file")?; + let object = objects.publish(&staging)?; + std::fs::remove_file(staging)?; + #[cfg(unix)] + let executable = { + use cap_std::fs::MetadataExt; + metadata.mode() & 0o111 != 0 + }; + #[cfg(not(unix))] + let executable = false; + entries.push(WorkspaceEntry::File { + path: path.clone(), + object, + executable, + }); + } + let mut after = BTreeMap::new(); + scan(&dir, "", &mut after, 0)?; + let named = Dir::open_ambient_dir(&source.root, cap_std::ambient_authority())?; + if before != after + || before_root != stamp(&dir.dir_metadata()?)? + || before_root != stamp(&named.dir_metadata()?)? + { + return Err(invalid("checkpoint workspace changed during capture")); + } + let encoded = serde_json::to_vec(&Inventory { + version: 1, + entries, + })?; + if encoded.len() as u64 > MAX_INVENTORY { + return Err(invalid("checkpoint workspace inventory limit")); + } + let mut file = objects.create(request, "workspace")?; + file.write_all(&encoded)?; + file.sync_all()?; + drop(file); + Ok(WorkspaceSnapshot { + identity: source.identity.clone(), + consistency: source.consistency, + inventory: objects.publish(&objects.staging(request, "workspace")?)?, + }) +} + +pub(super) fn inventory( + objects: &Objects, + snapshot: &WorkspaceSnapshot, +) -> Result, StoreError> { + if snapshot.inventory.bytes > MAX_INVENTORY { + return Err(invalid("checkpoint workspace inventory limit")); + } + let inventory: Inventory = serde_json::from_reader(objects.read(&snapshot.inventory)?)?; + if inventory.version != 1 || inventory.entries.len() > MAX_ENTRIES { + return Err(invalid("unsupported checkpoint workspace inventory")); + } + let mut paths = BTreeSet::new(); + let mut directories = BTreeSet::new(); + let mut total = 0u64; + for entry in &inventory.entries { + let path = match entry { + WorkspaceEntry::Directory { path } | WorkspaceEntry::File { path, .. } => path, + }; + relative(path)?; + // Reject case aliases even when the publishing filesystem happens to allow them. + if !paths.insert(path.to_lowercase()) { + return Err(invalid("checkpoint workspace path collision")); + } + if let Some((parent, _)) = path.rsplit_once('/') + && !directories.contains(parent) + { + return Err(invalid("checkpoint workspace parent missing")); + } + match entry { + WorkspaceEntry::Directory { path } => { + directories.insert(path.as_str()); + } + WorkspaceEntry::File { object, .. } => { + total = total + .checked_add(object.bytes) + .ok_or_else(|| invalid("checkpoint workspace size overflow"))?; + if total > MAX_BYTES { + return Err(invalid("checkpoint workspace size limit")); + } + } + } + } + Ok(inventory.entries) +} + +pub(super) fn verify(objects: &Objects, snapshot: &WorkspaceSnapshot) -> Result<(), StoreError> { + for entry in inventory(objects, snapshot)? { + if let WorkspaceEntry::File { object, .. } = entry { + objects.read(&object)?; + } + } + Ok(()) +} + +/// The caller supplies an exclusively owned, empty destination; never a user project. +pub(super) fn materialize( + objects: &Objects, + snapshot: &WorkspaceSnapshot, + destination: &Path, +) -> Result<(), StoreError> { + let dir = Dir::open_ambient_dir(destination, cap_std::ambient_authority())?; + materialize_in(objects, snapshot, &dir) +} +pub(super) fn materialize_in( + objects: &Objects, + snapshot: &WorkspaceSnapshot, + dir: &Dir, +) -> Result<(), StoreError> { + let entries = inventory(objects, snapshot)?; + if dir.entries()?.next().is_some() { + return Err(invalid("checkpoint workspace destination is occupied")); + } + for entry in entries { + match entry { + WorkspaceEntry::Directory { path } => { + let (parent, name) = parent(dir, &path)?; + parent.create_dir(&name)?; + } + WorkspaceEntry::File { + path, + object, + executable, + } => { + let (parent, name) = parent(dir, &path)?; + let mut source = objects.read(&object)?; + let mut options = OpenOptions::new(); + options + .write(true) + .create_new(true) + .follow(FollowSymlinks::No); + #[cfg(unix)] + { + use cap_std::fs::OpenOptionsExt; + options.mode(if executable { 0o700 } else { 0o600 }); + } + #[cfg(not(unix))] + let _ = executable; + let mut target = parent.open_with(name, &options)?; + std::io::copy(&mut source, &mut target)?; + target.sync_all()?; + } + } + } + // Persist directory entries from leaves toward the root before publishing readiness. + fn sync(dir: &Dir) -> Result<(), StoreError> { + for entry in dir.entries()? { + let entry = entry?; + if entry.file_type()?.is_dir() { + sync(&dir.open_dir_nofollow(entry.file_name())?)?; + } + } + dir.try_clone()?.into_std_file().sync_all()?; + Ok(()) + } + sync(dir) +} diff --git a/platform/event-log/src/root_store/gates.rs b/platform/event-log/src/root_store/gates.rs index 1cad4197e37..a483cc40e16 100644 --- a/platform/event-log/src/root_store/gates.rs +++ b/platform/event-log/src/root_store/gates.rs @@ -30,6 +30,7 @@ use tokio_util::sync::CancellationToken; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(super) enum Key { Creation(String), + CheckpointLoad(String), // Session gates precede binding gates for operations that need both. Session(String), Work(String), diff --git a/platform/event-log/src/root_store/session_metadata.rs b/platform/event-log/src/root_store/session_metadata.rs index 12eced09e7b..ee838da5a5b 100644 --- a/platform/event-log/src/root_store/session_metadata.rs +++ b/platform/event-log/src/root_store/session_metadata.rs @@ -131,6 +131,7 @@ impl SessionStore { false, permission_time, Some(root_app_control), + None, move |current| { *current = candidate; Ok(()) diff --git a/platform/event-log/src/sessions/metadata.rs b/platform/event-log/src/sessions/metadata.rs index 3bf4c57acb1..a168e9fe9cc 100644 --- a/platform/event-log/src/sessions/metadata.rs +++ b/platform/event-log/src/sessions/metadata.rs @@ -104,7 +104,7 @@ impl EventLog { T: Serialize + DeserializeOwned + Clone + PartialEq + Send + 'static, F: FnOnce(&mut T) -> Result<(), StoreError> + Send + 'static, { - self.mutate_session_metadata(id, expected, false, None, None, update) + self.mutate_session_metadata(id, expected, false, None, None, None, update) .await } @@ -122,7 +122,7 @@ impl EventLog { F: FnOnce(&mut T) -> Result<(), StoreError> + Send + 'static, { validate_time(now)?; - self.mutate_session_metadata(id, expected, false, Some(now), None, update) + self.mutate_session_metadata(id, expected, false, Some(now), None, None, update) .await } @@ -137,13 +137,14 @@ impl EventLog { where T: Serialize + DeserializeOwned + Clone + PartialEq + Send + 'static, { - self.mutate_session_metadata(id, expected, true, None, None, move |current| { + self.mutate_session_metadata(id, expected, true, None, None, None, move |current| { *current = configuration; Ok(()) }) .await } + #[allow(clippy::too_many_arguments)] // One SQL transaction owns metadata and optional checkpoint receipt. pub(crate) async fn mutate_session_metadata( &self, id: &str, @@ -151,6 +152,7 @@ impl EventLog { always_advance: bool, permission_time: Option, root_app_control: Option>, + checkpoint: Option, update: F, ) -> Result, StoreError> where @@ -169,6 +171,9 @@ impl EventLog { Box::pin(async move { let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?; super::removal::require_mutable(&mut tx, &id).await?; + if let Some(commit) = &checkpoint { + commit.validate(&mut tx).await?; + } let record: SessionRecord = read(&mut tx, &id) .await? .ok_or(StoreError::SessionNotFound)?; @@ -247,6 +252,9 @@ impl EventLog { let record = read(&mut tx, &id) .await? .ok_or(StoreError::SessionNotFound)?; + if let Some(commit) = checkpoint { + commit.record(&mut tx, next_revision).await?; + } tx.commit().await.map_err(StoreError::CommitUnknown)?; if approval_revoked { commits.activations(); diff --git a/plugins/agent/api/README.md b/plugins/agent/api/README.md index 2f0cad7adfd..9cc168a2746 100644 --- a/plugins/agent/api/README.md +++ b/plugins/agent/api/README.md @@ -90,4 +90,36 @@ Preview the source, then publish with its `confirmedSubtreeDigest` and an explic For reads, first obtain a head, then request its exact revision. A moved head fails rather than selecting latest; old success receipts do not grant reads of old revisions. An admitted read retains its verified file and may finish if the head moves afterward. The subscription emits `native_history_v1` metadata, contiguous Base64 `chunk` frames (at most 32 KiB decoded), then `end` with the complete-object SHA-256 and byte length. The payload is native `MAKA-SESSION` v2, not tar.gz or the former TypeScript checkpoint format. Require matching metadata, contiguous offsets, digest, End, successful null completion and cleanup before accepting a download. Missing End is incomplete delivery. Backpressure bounds queued chunks; cancellation waits for the original reader to drain. No caller-provided file path is accepted. -These are management methods, not model tools. Scope comes from the admitted Agent assembly; model-origin calls and foreign namespaces are rejected. The checkpoint includes only the native bundle's historical closure and referenced artifacts, not project files, all Memory/Goal state, execution authority or restore/activation. Publication currently requires the Root store's Unix implementation; unsupported platforms fail explicitly. Private immutable objects are conservatively retained; automatic triggers, UI, retention/GC and workspace capture remain separate work. +These are management methods, not model tools. Scope comes from the admitted Agent assembly; model-origin calls and foreign namespaces are rejected. `publishHistoryCheckpoint` includes only the native bundle's historical closure and referenced artifacts. Workspace and bounded recovery use the explicit methods below; neither transfers all Memory/Goal state or cross-Host execution authority. Publication currently requires the Root store's Unix implementation; unsupported platforms fail explicitly. Private immutable objects are conservatively retained; automatic triggers, UI and retention/GC remain separate work. Workspace publication and bounded local recovery use the explicit methods below. + +### Exact read-only loading + +Call `loadHistoryCheckpoint` as a prepared Host mutation with `{ sessionId, revision }` from `historyCheckpointHead`. Retain its original `RecoveryRef` and input. The current retained revision is verified and claimed before destination writes; a moved Head rejects a new load. Once claimed, recovery finishes from the pinned manifest and bundle even if Head advances. The `ready` result contains an `inspection` locator and the imported `sessionIds`. + +The inspection preserves historical Session IDs in a separate private database. It does not overwrite live Sessions or register runnable copies in the ordinary catalog. No model, tool, pending-message consumer or background Agent task is started by loading. Source configuration is historical evidence, never a destination execution grant. For `native_session_v1`, loading also materializes the recorded workspace in private inspection storage and returns its consistency metadata. It never rebinds the live Session, starts execution, or imports into another Host. + +Use `historyCheckpointLoadReceipt` with `{ reference, input }` after an uncertain response. It observes the original Host operation and domain load independently, without starting another load. `preparing` awaits recovery from the original materials; `unavailable` requires investigation. A domain `ready` does not imply Host outcome/cleanup confirmation. Root startup resumes pending imports before serving inspections; an existing native import receipt reconciles a committed destination whose load receipt was lost. + +These queries take the returned `inspection` and a member `sessionId`: + +| Method | Additional input | Result | +| --- | --- | --- | +| `loadedHistoryCheckpointHistory` | `position`, optional `includeThinking` | Existing bounded `HistoryPage`, including tool-result text | +| `loadedHistoryCheckpointArtifacts` | `offset` | At most 32 artifact descriptors and `nextOffset` | +| `loadedHistoryCheckpointArtifact` | `artifactId`, byte `offset` | Metadata and at most 32 KiB Base64 payload; null if not found | + +Use returned history cursors and artifact offsets to page without reading the whole checkpoint into memory. An inspection is retained independently of later Head changes. Inspection databases and immutable objects currently have no deletion/GC API; repeated **new** loads consume additional disk. Retrying the same original operation reuses its destination. These methods require the same management authorization and namespace as publication; locators are not bearer capabilities. + +### Workspace publication and local continuation + +`publishSessionCheckpoint` is a Host mutation with `{ history: { sessionId, expectedRevision, confirmedSubtreeDigest }, workspaceConsistency }`; `sessionCheckpointReceipt` takes its original `{ reference, input }`. The ordinary native Session must be idle and have a single Workspace. The first implementation rejects publication/restoration while this native Owner has active work and holds its admission gate during copying; large projects can delay new messages. The source path comes from its validated Host binding. This produces `native_session_v1`: the native history bundle plus an immutable workspace inventory and content-addressed file objects. The history subscription still streams only the bundle; its first frame identifies the variant and workspace inventory. + +`workspaceConsistency` is required and is either `caller_confirmed_quiescent` or `unverified`. **Neither mode fences external writers or requires Apple entitlements.** Display `external_writers_not_fenced` to the user: keep editors, terminals, other agents and background writers stopped throughout capture. Caller confirmation is an assertion, not an OS snapshot proof. Without that assurance the result is explicitly unverified and must not be presented as a consistent/trusted checkpoint. Detected source changes abort publication in both modes; a successful hash check proves the saved bytes' integrity, not that files came from one point in time. The mode and warning survive manifest storage, loading and restoration. + +The v1 file policy excludes `.git`, `node_modules`, `.DS_Store`, `__pycache__`, `.cache`, and `.maka-workspace.json`. Git metadata, dependencies and the old local workspace marker are not portable task state in this format. Known credential filenames (including real `.env` files, private keys and credential directories), symlinks and special files are rejected; filename checks are not a universal secret scanner. Limits are 20,000 entries, 2 GiB total file content, 64 directory levels and a 16 MiB inventory. Regular files, executable bits and empty directories are preserved. Unsupported/nonportable paths fail instead of being rewritten. + +`restoreSessionCheckpoint` takes `{ checkpoint: { sessionId, revision }, parent: DirectorySelection, permissions, backend, acceptExternalWritersRisk: true }`. Select an allowed parent directory; the Host creates a **new** `maka-restored-…` child and never overwrites the original project. Only an ordinary native Session in the **same Root**, still at the captured live revision, is supported. Advanced history, moved checkpoint Head, WorkHub, child Sessions, Goal state and executor backends are rejected. The target model and permissions are explicit; old grants are retired. An unverified source remains unverified after restoration. + +The restore atomically commits the new workspace binding and original restore receipt. It keeps the Session ID and existing checkpoint Head; a later publication uses that Head's revision as its CAS expectation. It does **not** roll back history or reconstruct a lost Root. Loading/rebinding does not execute a model or tool. Send an ordinary new message explicitly to continue; it uses the new Workspace. Cross-Root/Host authority handoff, arbitrary historical rollback, full auxiliary-state migration and mid-turn recovery are outside this implementation. + +After an uncertain restore response, use `sessionCheckpointRestoreReceipt` with the original `{ reference, input }`. Querying a committed receipt does not require the old destination path still to exist. A durable request alone does not own its destination: only a successfully copied and synced directory has its physical identity recorded. Retry and commit require that same directory, even if a replacement has identical contents. Before commit, occupied, altered or incomplete destinations fail closed and never rebind the Session; retain them for inspection and use a newly selected operation/destination after resolving the failed preparation. Restore also excludes in-flight Goal producers until the binding commits. No automatic deletion of user-visible recovery directories is performed. Private inspection databases, workspace copies and immutable file objects consume retained disk; automatic GC is not implemented. diff --git a/plugins/agent/api/README.zh-CN.md b/plugins/agent/api/README.zh-CN.md index ce91e985b69..74d13c3725e 100644 --- a/plugins/agent/api/README.zh-CN.md +++ b/plugins/agent/api/README.zh-CN.md @@ -90,4 +90,38 @@ macOS 上,活动订阅还会比较直接子项的元数据。每次扫描后 读取时先获取 Head,再指定其精确 revision;Head 已移动则失败,不自动选择最新。历史成功回执不授予旧 revision 读取能力。已经受理的读取持有校验后的文件,随后 Head 移动仍可完成。流依次返回 `native_history_v1` 元数据、连续偏移的 Base64 `chunk`(解码后每块最多 32 KiB)及含完整对象 SHA-256、字节数的 `end`。内容是原生 `MAKA-SESSION` v2,不是 tar.gz,也不兼容旧 TypeScript checkpoint 格式。接收方须检查元数据、连续偏移、摘要、End、成功的 null 完成及清理结果;缺少 End 就是不完整传输。背压限制排队数据块,取消等待原读取工作排空,不接受调用方任意文件路径。 -这些方法属于管理入口,不是模型工具。Scope 来自已获准的 Agent 装配;模型来源调用和其他 namespace 被拒绝。存档只包含原生 Bundle 的历史闭包及关联 Artifact,不包含项目文件、完整 Memory/Goal 状态、执行授权或恢复激活。发布目前依赖 Root 存储的 Unix 实现,不支持的平台显式失败。不可变对象保守保留,自动触发、UI、retention/GC 和 Workspace 捕获另行推进。 +这些方法属于管理入口,不是模型工具。Scope 来自已获准的 Agent 装配;模型来源调用和其他 namespace 被拒绝。`publishHistoryCheckpoint` 只包含原生 Bundle 的历史闭包及关联 Artifact;Workspace 和受限恢复使用下文的显式方法。两者均不包含完整 Memory/Goal 状态或跨 Host 执行授权。发布目前依赖 Root 存储的 Unix 实现,不支持的平台显式失败。不可变对象保守保留,自动触发、UI 和 retention/GC 另行推进。 + +### 精确只读加载 + +从 `historyCheckpointHead` 取得 `{ sessionId, revision }`,通过 Host 的 mutation 准备与确认调用 `loadHistoryCheckpoint`,保存原 `RecoveryRef` 和输入。写入目标之前先校验并登记当前 retained revision;Head 已移动则拒绝新的加载。登记成功后,即使 Head 推进,恢复仍使用已固定的 Manifest 和 Bundle。`ready` 返回 `inspection` 定位信息及导入的 `sessionIds`。 + +检查副本在独立私有数据库中保留历史 Session ID,不覆盖实时 Session,也不向普通目录注册可运行副本。加载不会启动模型、工具、待处理消息消费者或 Agent 后台任务。源配置只作为历史证据,不成为目标执行授权。对 `native_session_v1` 还会在私有检查区物化 Workspace,但不自动切换实时目录或继续执行,也不向另一 Host 导入。 + +响应未知时,携带 `{ reference, input }` 查询 `historyCheckpointLoadReceipt`。它分别返回原 Host 操作观测与领域加载结果,不启动另一加载。`preparing` 等待从原材料恢复,`unavailable` 需要排查;领域 `ready` 不代表 Host 已确认结果或清理。Root 在开放检查接口前恢复未完成导入;若目标已提交而加载回执丢失,使用原生导入回执找回原结果。 + +以下查询携带返回的 `inspection` 和其中一个成员 `sessionId`: + +| 方法 | 其他输入 | 结果 | +| --- | --- | --- | +| `loadedHistoryCheckpointHistory` | `position`,可选 `includeThinking` | 既有有界 `HistoryPage`,包含工具结果文本 | +| `loadedHistoryCheckpointArtifacts` | `offset` | 最多 32 条 Artifact 元数据及 `nextOffset` | +| `loadedHistoryCheckpointArtifact` | `artifactId`、字节 `offset` | 元数据及最多 32 KiB 的 Base64 内容;不存在返回 null | + +使用返回的历史游标和 Artifact 偏移分页,无需把整个 checkpoint 放入内存。检查副本独立于后续 Head 变化保留。目前检查数据库和不可变对象没有删除/GC API;不断发起**新**加载会增加磁盘占用,原操作重试复用原目标。这些方法与发布使用相同的管理授权和 namespace;定位信息本身不授予访问权。 + +### Workspace 发布与本地继续执行 + +`publishSessionCheckpoint` 是 Host mutation,输入 `{ history: { sessionId, expectedRevision, confirmedSubtreeDigest }, workspaceConsistency }`;`sessionCheckpointReceipt` 使用原始 `{ reference, input }` 查询回执。只支持空闲的普通 native Session 和单个 Workspace。首版在该 native Owner 存在活跃任务时拒绝发布/恢复,复制期间持有准入锁;大项目会延迟新消息,不能承诺无性能影响。路径来自 Host 验证过的 Session 绑定。产物 `native_session_v1` 包括历史 Bundle、不可变 Workspace 清单和按内容寻址的文件对象。历史订阅仍只传输 Bundle,首帧会标明类型及 Workspace 清单引用。 + +必须显式选择 `caller_confirmed_quiescent`(调用方确认复制全程已停写)或 `unverified`(不能保证)。**两者都不隔离外部写入者,也不要求 Apple entitlement。** 客户端应显示 `external_writers_not_fenced` 提示:捕获期间需暂停编辑器、终端、其他 Agent 和后台程序的写入。用户确认只是前提声明,不是系统快照证明;无法保证时不得把产物宣称为一致、可信的 checkpoint。两种模式检测到变化都中止发布;摘要校验仅证明保存的字节完整,不能证明各文件来自同一时刻。此标记随 Manifest、加载和恢复保留。 + +v1 文件策略排除 `.git`、`node_modules`、`.DS_Store`、`__pycache__`、`.cache` 和 `.maka-workspace.json`;不携带 Git 元数据、依赖目录或旧本地 Workspace 标记。发现已知凭据文件名(真实 `.env`、私钥、凭据目录等)、符号链接或特殊文件则拒绝;这不是通用的秘密内容扫描器。上限为 20,000 项、文件正文合计 2 GiB、64 层目录、16 MiB 清单。保留普通文件、可执行位和空目录;不可移植路径显式拒绝。 + +`loadHistoryCheckpoint` 对 `native_session_v1` 还会在 Root 私有检查区物化文件,并返回一致性元数据;不会自动替换实时 Session 的目录或开始执行。 + +`restoreSessionCheckpoint` 输入为 `{ checkpoint: { sessionId, revision }, parent: DirectorySelection, permissions, backend, acceptExternalWritersRisk: true }`。Host 在获准的父目录中新建 `maka-restored-…` 子目录,不覆盖原项目。首版仅支持**同 Root、实时 revision 尚未超过存档点**的普通 native Session;历史已推进、Head 已移动、WorkHub、子 Session、Goal 状态和 executor backend 都拒绝。显式选择目标模型与权限,撤销旧授权;unverified 不会因恢复变成可信。 + +恢复在同一事务提交目录绑定与原操作回执,保留 Session ID 和 checkpoint Head,下一次发布仍以原 Head revision 做 CAS。不会回滚新历史,也不重建已丢失的 Root;加载/重新绑定不会自动调用模型或工具,显式发送下一条普通消息后才从新目录执行。跨 Root/Host 权威交接、任意历史回滚、完整辅助状态迁移和轮次中间恢复不在本次范围。 + +回执未知时,使用原始 `{ reference, input }` 调用 `sessionCheckpointRestoreReceipt`;已提交回执不依赖目标目录仍在原位置。登记恢复请求不代表拥有目标目录;只有复制完成且落盘成功后,才记录实际创建目录的物理身份。重试与提交都要求仍是该目录,即便替换目录内容完全相同也拒绝。提交前若目标已被占用、不完整或被改变,则保持原 Session 不变,保留目录供检查;处理失败准备后使用新的操作/目标目录。恢复还会排除未完成的 Goal 写入,并持续到目录绑定提交。不自动删除用户可见的恢复目录;检查数据库、Workspace 副本和文件对象均占用持久磁盘,尚无自动 GC。 diff --git a/plugins/agent/api/src/contract.rs b/plugins/agent/api/src/contract.rs index d5355f3caaa..c881d5701c5 100644 --- a/plugins/agent/api/src/contract.rs +++ b/plugins/agent/api/src/contract.rs @@ -1484,6 +1484,55 @@ pub enum HistoryPage { }, } +impl From for HistoryPage { + fn from(value: maka_agent_ports::session::history::Page) -> Self { + let maka_agent_ports::session::history::Page::Ready { + through, + chunks, + before, + after, + } = value; + HistoryPage::Ready { + through, + before, + after, + chunks: chunks + .into_iter() + .map(|chunk| HistoryChunk { + model_part: chunk.model_part, + has_references: chunk.has_references, + final_output: chunk.final_output, + model: chunk.model, + origin: chunk.origin, + compaction: chunk.compaction, + attachments: chunk + .attachments + .into_iter() + .map(|attachment| HistoryAttachment { + name: attachment.name, + mime_type: attachment.mime_type, + bytes: attachment.bytes, + kind: attachment.kind, + }) + .collect(), + timing: chunk.timing, + field: chunk.field, + message_id: chunk.message_id, + turn_id: chunk.turn_id, + timestamp: chunk.timestamp, + role: chunk.role, + order: chunk.order, + position: chunk.position, + offset: chunk.offset, + total_bytes: chunk.total_bytes, + text: chunk.text, + tool: chunk.tool, + }) + .collect(), + } + } +} + #[derive(Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(rename = "AgentHistorySourceRead")] @@ -1856,6 +1905,15 @@ pub fn contract() -> Contract { "readHistoryCheckpoint".into(), method::>(MethodMode::Subscription), ), + ("loadHistoryCheckpoint".into(), method::(MethodMode::Mutation)), + ("restoreSessionCheckpoint".into(), method::(MethodMode::Mutation)), + ("sessionCheckpointRestoreReceipt".into(), method::(MethodMode::Query)), + ("publishSessionCheckpoint".into(), method::(MethodMode::Mutation)), + ("sessionCheckpointReceipt".into(), method::(MethodMode::Query)), + ("historyCheckpointLoadReceipt".into(), method::(MethodMode::Query)), + ("loadedHistoryCheckpointHistory".into(), method::(MethodMode::Query)), + ("loadedHistoryCheckpointArtifacts".into(), method::(MethodMode::Query)), + ("loadedHistoryCheckpointArtifact".into(), method::>(MethodMode::Query)), ( "fork".into(), method::(MethodMode::Mutation), diff --git a/plugins/agent/api/src/contract/checkpoints.rs b/plugins/agent/api/src/contract/checkpoints.rs index 9c75f9d6bc8..14c5bd22c53 100644 --- a/plugins/agent/api/src/contract/checkpoints.rs +++ b/plugins/agent/api/src/contract/checkpoints.rs @@ -17,12 +17,66 @@ * under the License. */ -//! History-only checkpoints; neither workspace snapshots nor execution authority. +//! Explicit history/Workspace checkpoints, private inspections and bounded local recovery. use maka_plugin_core::operation::{OperationObservation, RecoveryRef}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[schemars(rename = "AgentCheckpointWorkspaceConsistency")] +pub enum CheckpointWorkspaceConsistency { + /// The caller confirms all other writers remain paused for the entire copy. + CallerConfirmedQuiescent, + /// Files may not represent one instant; successful digest checks do not change this. + Unverified, +} + +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointPublish")] +pub struct SessionCheckpointPublish { + pub history: HistoryCheckpointPublish, + /// Must be explicitly selected. Maka does not fence other applications. + pub workspace_consistency: CheckpointWorkspaceConsistency, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointPublication")] +pub struct SessionCheckpointPublication { + pub publication: HistoryCheckpointPublication, + pub workspace_consistency: CheckpointWorkspaceConsistency, + /// Stable warning, present even with a caller attestation. + pub warning: CheckpointWorkspaceWarning, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[schemars(rename = "AgentCheckpointWorkspaceWarning")] +pub enum CheckpointWorkspaceWarning { + /// Display to users: pause editors, terminals and other writers for the entire + /// capture. Hash verification is not a point-in-time consistency guarantee. + ExternalWritersNotFenced, +} + +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointReceiptRead")] +pub struct SessionCheckpointReceiptRead { + pub reference: RecoveryRef, + pub input: SessionCheckpointPublish, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointReceipt")] +pub struct SessionCheckpointReceipt { + pub operation: Option, + pub checkpoint: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[schemars(rename = "AgentHistoryCheckpointPublish")] @@ -123,6 +177,13 @@ pub struct HistoryCheckpointRead { )] #[schemars(rename = "AgentHistoryCheckpointFrame")] pub enum HistoryCheckpointFrame { + /// The streamed payload is history; Workspace is a separate manifest component. + NativeSessionV1 { + head: HistoryCheckpointHead, + inventory: HistoryCheckpointInventory, + bundle: HistoryCheckpointObject, + workspace: CheckpointWorkspaceSnapshot, + }, /// The following bytes use the MAKA-SESSION v2 history format. NativeHistoryV1 { head: HistoryCheckpointHead, @@ -142,6 +203,125 @@ pub enum HistoryCheckpointFrame { }, } +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointInspection")] +pub struct CheckpointInspection { + #[schemars(length(min = 1))] + pub source_session_id: String, + pub operation_id: Uuid, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointLoad")] +pub struct CheckpointLoad { + pub head: HistoryCheckpointHead, + pub inspection: CheckpointInspection, + pub state: CheckpointLoadState, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde( + tag = "kind", + rename_all = "snake_case", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +#[schemars(rename = "AgentCheckpointLoadState")] +pub enum CheckpointLoadState { + Preparing, + Ready { + session_ids: Vec, + workspace: Option, + }, + Unavailable, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointWorkspaceSnapshot")] +pub struct CheckpointWorkspaceSnapshot { + pub identity: String, + pub consistency: CheckpointWorkspaceConsistency, + pub warning: CheckpointWorkspaceWarning, + pub inventory: HistoryCheckpointObject, +} +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointLoadReceiptRead")] +pub struct CheckpointLoadReceiptRead { + pub reference: RecoveryRef, + pub input: HistoryCheckpointRead, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointLoadReceipt")] +pub struct CheckpointLoadReceipt { + pub operation: Option, + pub load: Option, +} +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointHistoryRead")] +pub struct CheckpointHistoryRead { + pub inspection: CheckpointInspection, + #[schemars(length(min = 1))] + pub session_id: String, + pub position: super::HistoryPosition, + #[serde(default)] + pub include_thinking: bool, +} +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointArtifactsRead")] +pub struct CheckpointArtifactsRead { + pub inspection: CheckpointInspection, + #[schemars(length(min = 1))] + pub session_id: String, + #[schemars(range(max = 9_007_199_254_740_991u64))] + pub offset: u64, +} +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointArtifactRead")] +pub struct CheckpointArtifactRead { + pub inspection: CheckpointInspection, + #[schemars(length(min = 1))] + pub session_id: String, + #[schemars(length(min = 1))] + pub artifact_id: String, + #[schemars(range(max = 9_007_199_254_740_991u64))] + pub offset: u64, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointArtifact")] +pub struct CheckpointArtifact { + pub id: String, + pub name: String, + pub mime_type: Option, + #[schemars(range(max = 9_007_199_254_740_991u64))] + pub bytes: u64, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointArtifacts")] +pub struct CheckpointArtifacts { + pub artifacts: Vec, + #[schemars(with = "maka_plugin_core::wire_schema::RequiredNullable")] + pub next_offset: Option, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentCheckpointArtifactChunk")] +pub struct CheckpointArtifactChunk { + pub artifact: CheckpointArtifact, + #[schemars(range(max = 9_007_199_254_740_991u64))] + pub offset: u64, + /// Base64, at most 32 KiB decoded. Empty only at the end. + #[schemars(length(max = 43692))] + pub data: String, +} + #[cfg(test)] mod tests { use super::*; @@ -186,4 +366,79 @@ mod tests { assert!(!read.is_valid(&json!({"sessionId":"source","revision":23}))); assert!(read.is_valid(&json!({"sessionId":"source","revision":Uuid::new_v4()}))); } + #[test] + fn checkpoint_load_requires_exact_revision_and_never_accepts_execution_authority() { + let Interface::Service { methods } = crate::contract().interface else { + unreachable!() + }; + assert_eq!(methods["loadHistoryCheckpoint"].mode, MethodMode::Mutation); + for method in [ + "historyCheckpointLoadReceipt", + "loadedHistoryCheckpointHistory", + "loadedHistoryCheckpointArtifacts", + "loadedHistoryCheckpointArtifact", + ] { + assert_eq!(methods[method].mode, MethodMode::Query); + } + let validator = jsonschema::validator_for(&methods["loadHistoryCheckpoint"].input).unwrap(); + let valid = json!({"sessionId":"source", "revision":Uuid::new_v4()}); + assert!(validator.is_valid(&valid)); + assert!(!validator.is_valid(&json!({"sessionId":"source"}))); + for (key, value) in [ + ("revision", json!(null)), + ("path", json!("/tmp/target")), + ("activate", json!(true)), + ("configuration", json!({})), + ("scope", json!({})), + ] { + let mut invalid = valid.clone(); + invalid[key] = value; + assert!(!validator.is_valid(&invalid), "{key}"); + } + let validator = + jsonschema::validator_for(&methods["loadedHistoryCheckpointArtifact"].input).unwrap(); + let mut valid = json!({"inspection":{"sourceSessionId":"source","operationId":Uuid::new_v4()},"sessionId":"source","artifactId":"a","offset":0}); + assert!(validator.is_valid(&valid)); + valid["offset"] = json!(9_007_199_254_740_992u64); + assert!(!validator.is_valid(&valid)); + } +} + +/// Restore files into a new child of a selected directory and rebind the same, +/// unchanged local Session. It never starts a model/tool call or rewinds history. +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointRestore")] +pub struct SessionCheckpointRestore { + pub checkpoint: HistoryCheckpointRead, + pub parent: super::DirectorySelection, + pub permissions: super::WorkspacePermissions, + pub backend: super::BackendPreference, + pub accept_external_writers_risk: bool, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointRestored")] +pub struct SessionCheckpointRestored { + pub operation: Uuid, + pub session_id: String, + #[schemars(range(max = 9_007_199_254_740_991u64))] + pub session_revision: u64, + pub head: HistoryCheckpointHead, + pub workspace: CheckpointWorkspaceSnapshot, + pub path: String, +} +#[derive(Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointRestoreReceiptRead")] +pub struct SessionCheckpointRestoreReceiptRead { + pub reference: RecoveryRef, + pub input: SessionCheckpointRestore, +} +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename = "AgentSessionCheckpointRestoreReceipt")] +pub struct SessionCheckpointRestoreReceipt { + pub operation: Option, + pub restore: Option, } diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md index 2a4b33c42c3..a376e3cc037 100644 --- a/plugins/agent/native/README.md +++ b/plugins/agent/native/README.md @@ -27,8 +27,10 @@ The [service adapter](src/owner/checkpoints.rs) exposes explicit management meth with Host admission, original receipt authorization and bounded subscription reads. Publication receipts resolve checkpoint success without fabricating generic Host cleanup confirmation. See the [API workflow](../api/README.md#history-checkpoint-management) -and [manual baseline](src/owner/tests/checkpoints/benchmark.rs). Workspace capture, -restore/activation and model tools are outside this API. +and [manual baseline](src/owner/tests/checkpoints/benchmark.rs). Model tools and +cross-Root activation remain outside this API. + +The [inspection adapter](src/owner/checkpoints/load.rs) adds exact checkpoint loading and bounded history/artifact queries through the same Host management boundary. Loaded history stays in isolated storage with its original IDs; it is never registered with this Owner's execution or message recovery. Query `historyCheckpointLoadReceipt` using the original operation and input after an uncertain reply. Workspace publication and explicit same-Root directory restoration are handled by the [Session adapter](src/owner/checkpoints/session.rs) and [restore adapter](src/owner/checkpoints/restore.rs). They retain conditional consistency warnings, require explicit target permissions/model selection, and never auto-send a message. See the [supported scope](../api/README.md#workspace-publication-and-local-continuation). `agent.fork` captures a source revision and an explicit `position`: `end`, `through_turn`, or `before_turn` with a logical Turn ID, or `start` for empty history. `before_turn` excludes the complete target Turn and accepts the first Turn to create an empty conversation. The Host resolves the boundary from canonical history in the copy transaction; clients do not need a preceding Turn or the sampled history landmarks. The returned branch retains that position, and the source conversation is unchanged. diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md index 16fcbe287e4..d4d558ab09f 100644 --- a/plugins/agent/native/README.zh-CN.md +++ b/plugins/agent/native/README.zh-CN.md @@ -26,7 +26,9 @@ [服务适配层](src/owner/checkpoints.rs)通过 Host 准入、原操作回执授权和有界订阅读取提供显式管理方法。 领域回执可确认 checkpoint 成功,但不伪造通用 Host 的清理确认。 参见 [API 工作流](../api/README.zh-CN.md#历史-checkpoint-管理)和[手动性能基线](src/owner/tests/checkpoints/benchmark.rs)。 -Workspace 捕获、恢复激活和模型工具不在此 API 范围内。 +模型工具和跨 Root 恢复激活不在此 API 范围内;Workspace 与同 Root 恢复见下文。 + +[检查适配层](src/owner/checkpoints/load.rs)通过相同 Host 管理边界提供精确加载及有界历史、Artifact 查询。加载的历史保留原 ID,放在隔离存储中,不向本 Owner 的执行或消息恢复注册。响应未知时使用原操作及原输入查询 `historyCheckpointLoadReceipt`。加载本身不授予执行能力;同 Root 恢复要求显式目标权限与模型,跨 Host 身份交接及系统级外部停写仍未提供。 `agent.fork` 捕获源会话版本和显式 `position`:`end`、带逻辑 Turn ID 的 `through_turn` 或 `before_turn`,以及表示空历史的 `start`。`before_turn` 排除目标完整轮次,指定第一轮可创建空对话。Host 在复制事务中从规范历史计算边界,客户端无需寻找上一轮或依赖采样的历史导航点。返回的分支保留该位置,源会话保持不变。 @@ -160,3 +162,5 @@ ChatGPT 冷缓存或到期刷新在逻辑请求准备时等待,后台压缩的 订阅 Provider 仅在连接开启该选项时,在解析后的能力中选择原生压缩。关闭后仍可回放已有原生 checkpoint;下一次压缩生成文字摘要。Host 冻结一个协议操作,Responses 适配器负责追加触发命令, 并只在流完成时返回有类型的 `NativeCompaction`。Engine 和 EventLog 无需再从 provider options JSON 提取结果。 检查点头部只保留来源步骤引用、内容摘要和 token 估算,baseline 读取时从原始请求派生 producer/model。 + +Workspace 发布与显式的同 Root 目录恢复分别见 [Session 适配器](src/owner/checkpoints/session.rs)和[恢复适配器](src/owner/checkpoints/restore.rs)。这两者保留条件一致性提示,要求明确的目标权限与模型,不自动发起消息。跨 Root 恢复仍未提供,详见 [API 支持范围](../api/README.zh-CN.md)。 diff --git a/plugins/agent/native/src/checkpoints.rs b/plugins/agent/native/src/checkpoints.rs index cfd4ac498b0..407e3eb9a5a 100644 --- a/plugins/agent/native/src/checkpoints.rs +++ b/plugins/agent/native/src/checkpoints.rs @@ -39,6 +39,7 @@ pub struct Capture { pub session_id: String, pub expected_checkpoint_revision: Option, pub confirmed_subtree_digest: String, + pub workspace: Option, } impl HistoryCheckpointPublisher { @@ -57,7 +58,7 @@ impl HistoryCheckpointPublisher { } } - async fn repository(&self, session: &str) -> Result { + pub(crate) async fn repository(&self, session: &str) -> Result { if !crate::owner::owns_session(&self.prefix, session) { return Err(StoreError::SessionNotFound); } @@ -66,7 +67,7 @@ impl HistoryCheckpointPublisher { .await } - fn request( + pub(crate) fn request( &self, capture: &Capture, operation: &RecoveryRef, @@ -78,7 +79,12 @@ impl HistoryCheckpointPublisher { || operation.source.deployment != self.scope.deployment || operation.namespace != self.scope.namespace || operation.export != maka_agent_api::EXPORT - || operation.method != "publishHistoryCheckpoint" + || operation.method + != if capture.workspace.is_some() { + "publishSessionCheckpoint" + } else { + "publishHistoryCheckpoint" + } { return Err(StoreError::InvalidTransition( "checkpoint operation belongs to another authority".into(), @@ -90,6 +96,7 @@ impl HistoryCheckpointPublisher { fingerprint: hex::encode(sha2::Sha256::digest(serde_json::to_vec(operation)?)), expected_revision: capture.expected_checkpoint_revision, confirmed_subtree_digest: capture.confirmed_subtree_digest.clone(), + workspace: capture.workspace.clone(), }) } @@ -104,6 +111,33 @@ impl HistoryCheckpointPublisher { self.request(capture, operation).map(|_| ()) } + pub(crate) fn load_request( + &self, + input: &maka_agent_api::contract::HistoryCheckpointRead, + operation: &RecoveryRef, + ) -> Result { + operation + .validate() + .map_err(|e| StoreError::InvalidTransition(e.to_string()))?; + if !crate::owner::owns_session(&self.prefix, &input.session_id) + || operation.source.state_domain != self.scope.state_domain + || operation.source.deployment != self.scope.deployment + || operation.namespace != self.scope.namespace + || operation.export != maka_agent_api::EXPORT + || operation.method != "loadHistoryCheckpoint" + { + return Err(StoreError::InvalidTransition( + "checkpoint load belongs to another authority".into(), + )); + } + use sha2::Digest; + Ok(maka_event_log::checkpoints::LoadOperation { + operation: operation.operation, + fingerprint: hex::encode(sha2::Sha256::digest(serde_json::to_vec(operation)?)), + revision: Revision(input.revision), + }) + } + pub async fn publish( &self, capture: Capture, @@ -184,6 +218,7 @@ mod tests { session_id: id.clone(), expected_checkpoint_revision: None, confirmed_subtree_digest: store.preview_bundle(&id).await.unwrap().subtree_digest, + workspace: None, }; let operation = RecoveryRef { source: DeploymentRef { diff --git a/plugins/agent/native/src/owner/checkpoints.rs b/plugins/agent/native/src/owner/checkpoints.rs index 72715832038..2d87d176de6 100644 --- a/plugins/agent/native/src/owner/checkpoints.rs +++ b/plugins/agent/native/src/owner/checkpoints.rs @@ -19,6 +19,9 @@ //! Authorized history publication and bounded reads through the Agent service. use super::*; +mod load; +mod restore; +mod session; use crate::checkpoints::{Capture, HistoryCheckpointPublisher}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use maka_capabilities::call::Scope; @@ -155,20 +158,39 @@ impl Owner { .checkout_exact(&input.session_id, storage::Revision(input.revision)) .await .map_err(failure)?; - let storage::Manifest::NativeHistory { + let workspace = checkout + .manifest + .workspace() + .cloned() + .map(workspace_snapshot); + let (storage::Manifest::NativeHistory { inventory: members, bundle, .. - } = checkout.manifest; + } + | storage::Manifest::NativeSession { + inventory: members, + bundle, + .. + }) = checkout.manifest; let expected = bundle.clone(); emit( &context, &owner, &scope, - HistoryCheckpointFrame::NativeHistoryV1 { - head: head(checkout.head), - inventory: inventory(members), - bundle: object(bundle), + if let Some(workspace) = workspace { + HistoryCheckpointFrame::NativeSessionV1 { + head: head(checkout.head), + inventory: inventory(members), + bundle: object(bundle), + workspace, + } + } else { + HistoryCheckpointFrame::NativeHistoryV1 { + head: head(checkout.head), + inventory: inventory(members), + bundle: object(bundle), + } }, ) .await?; @@ -245,6 +267,7 @@ fn capture(input: HistoryCheckpointPublish) -> Capture { session_id: input.session_id, expected_checkpoint_revision: input.expected_revision.map(storage::Revision), confirmed_subtree_digest: input.confirmed_subtree_digest, + workspace: None, } } fn object(value: storage::ObjectRef) -> HistoryCheckpointObject { @@ -253,6 +276,19 @@ fn object(value: storage::ObjectRef) -> HistoryCheckpointObject { bytes: value.bytes, } } +fn workspace_snapshot(w: storage::WorkspaceSnapshot) -> CheckpointWorkspaceSnapshot { + CheckpointWorkspaceSnapshot { + identity: w.identity, + inventory: object(w.inventory), + consistency: match w.consistency { + storage::WorkspaceConsistency::CallerConfirmedQuiescent => { + CheckpointWorkspaceConsistency::CallerConfirmedQuiescent + } + storage::WorkspaceConsistency::Unverified => CheckpointWorkspaceConsistency::Unverified, + }, + warning: CheckpointWorkspaceWarning::ExternalWritersNotFenced, + } +} fn head(value: storage::Head) -> HistoryCheckpointHead { HistoryCheckpointHead { revision: value.revision.0, diff --git a/plugins/agent/native/src/owner/checkpoints/load.rs b/plugins/agent/native/src/owner/checkpoints/load.rs new file mode 100644 index 00000000000..dae314827ee --- /dev/null +++ b/plugins/agent/native/src/owner/checkpoints/load.rs @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Host-admitted isolated inspection; no method here creates a runnable Session. +use super::*; + +impl Owner { + pub async fn load_history_checkpoint( + self: &Arc, + input: HistoryCheckpointRead, + operation: &RecoveryRef, + scope: &Scope, + ) -> Result { + management(scope)?; + let operation = operation.clone(); + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let publisher = owner.history_checkpoints(); + let request = publisher + .load_request(&input, &operation) + .map_err(failure)?; + let value = publisher + .repository(&input.session_id) + .await + .map_err(failure)? + .load_exact(request) + .await + .map_err(failure)?; + Ok(loaded( + value, + CheckpointInspection { + source_session_id: input.session_id, + operation_id: operation.operation, + }, + )) + }) + }) + .await + } + pub async fn history_checkpoint_load_receipt( + self: &Arc, + input: CheckpointLoadReceiptRead, + context: Context, + ) -> Result { + management(context.scope())?; + let scope = context.scope().clone(); + self.resource_work(&scope, move |owner, _| { + Box::pin(async move { + let publisher = owner.history_checkpoints(); + let request = publisher + .load_request(&input.input, &input.reference) + .map_err(failure)?; + let operation = context.recover(input.reference.clone())?.await?; + let load = if operation.is_some() { + publisher + .repository(&input.input.session_id) + .await + .map_err(failure)? + .load_receipt(request) + .await + .map_err(failure)? + .map(|value| { + loaded( + value, + CheckpointInspection { + source_session_id: input.input.session_id, + operation_id: input.reference.operation, + }, + ) + }) + } else { + None + }; + Ok(CheckpointLoadReceipt { operation, load }) + }) + }) + .await + } + pub async fn loaded_checkpoint_history( + self: &Arc, + input: CheckpointHistoryRead, + scope: &Scope, + ) -> Result { + management(scope)?; + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let page = owner + .history_checkpoints() + .repository(&input.inspection.source_session_id) + .await + .map_err(failure)? + .loaded_history( + input.inspection.operation_id, + input.session_id, + input.position, + input.include_thinking, + ) + .await + .map_err(failure)?; + Ok(HistoryPage::from(page)) + }) + }) + .await + } + pub async fn loaded_checkpoint_artifacts( + self: &Arc, + input: CheckpointArtifactsRead, + scope: &Scope, + ) -> Result { + management(scope)?; + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let page = owner + .history_checkpoints() + .repository(&input.inspection.source_session_id) + .await + .map_err(failure)? + .loaded_artifacts( + input.inspection.operation_id, + input.session_id, + input.offset, + ) + .await + .map_err(failure)?; + let next = input.offset + page.records.len() as u64; + Ok(CheckpointArtifacts { + next_offset: (next < page.total).then_some(next), + artifacts: page.records.into_iter().map(artifact).collect(), + }) + }) + }) + .await + } + pub async fn loaded_checkpoint_artifact( + self: &Arc, + input: CheckpointArtifactRead, + scope: &Scope, + ) -> Result, String> { + management(scope)?; + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let record = owner + .history_checkpoints() + .repository(&input.inspection.source_session_id) + .await + .map_err(failure)? + .loaded_artifact( + input.inspection.operation_id, + input.session_id, + input.artifact_id, + input.offset, + ) + .await + .map_err(failure)?; + Ok(record.map(|(record, chunk)| CheckpointArtifactChunk { + artifact: artifact(record), + offset: input.offset, + data: STANDARD.encode(chunk.bytes), + })) + }) + }) + .await + } +} +fn loaded(value: storage::LoadedCheckpoint, inspection: CheckpointInspection) -> CheckpointLoad { + CheckpointLoad { + head: head(value.head), + inspection, + state: match value.state { + storage::LoadState::Preparing => CheckpointLoadState::Preparing, + storage::LoadState::Ready { + session_ids, + workspace, + .. + } => CheckpointLoadState::Ready { + session_ids, + workspace: workspace.map(|w| CheckpointWorkspaceSnapshot { + identity: w.identity, + inventory: object(w.inventory), + consistency: match w.consistency { + storage::WorkspaceConsistency::CallerConfirmedQuiescent => { + CheckpointWorkspaceConsistency::CallerConfirmedQuiescent + } + storage::WorkspaceConsistency::Unverified => { + CheckpointWorkspaceConsistency::Unverified + } + }, + warning: CheckpointWorkspaceWarning::ExternalWritersNotFenced, + }), + }, + storage::LoadState::Unavailable => CheckpointLoadState::Unavailable, + }, + } +} +fn artifact(record: maka_agent_facts::artifact::Artifact) -> CheckpointArtifact { + CheckpointArtifact { + id: record.id, + name: record.name, + bytes: record.size_bytes, + mime_type: record.mime_type, + } +} diff --git a/plugins/agent/native/src/owner/checkpoints/restore.rs b/plugins/agent/native/src/owner/checkpoints/restore.rs new file mode 100644 index 00000000000..53666dc9e80 --- /dev/null +++ b/plugins/agent/native/src/owner/checkpoints/restore.rs @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Explicit same-Root workspace recovery; loading never admits a model call. +use super::*; + +fn restored(value: storage::RestoredSession) -> SessionCheckpointRestored { + SessionCheckpointRestored { + operation: value.operation, + session_id: value.session_id, + session_revision: value.session_revision, + head: head(value.prepared.head), + workspace: workspace_snapshot(value.prepared.workspace), + path: value.prepared.path.to_string_lossy().into_owned(), + } +} +impl Owner { + async fn restore_request( + &self, + input: &SessionCheckpointRestore, + operation: &RecoveryRef, + ) -> Result<(maka_event_log::root_store::HistoryCheckpoints, String), String> { + operation.validate().map_err(|e| e.to_string())?; + if !self.owns(&input.checkpoint.session_id) + || operation.method != "restoreSessionCheckpoint" + || operation.export != maka_agent_api::EXPORT + || operation.source.state_domain != self.data_source.state_domain + || operation.source.deployment != self.data_source.deployment + || operation.namespace != self.data_source.namespace + { + return Err("agent_checkpoint_restore_authority_mismatch".into()); + } + let repository = self + .history_checkpoints() + .repository(&input.checkpoint.session_id) + .await + .map_err(failure)?; + let fingerprint = hex::encode(Sha256::digest( + serde_json::to_vec(operation).map_err(|e| e.to_string())?, + )); + Ok((repository, fingerprint)) + } + + pub async fn restore_session_checkpoint( + self: &Arc, + input: SessionCheckpointRestore, + operation: &RecoveryRef, + scope: &Scope, + ) -> Result { + management(scope)?; + let operation = operation.clone(); + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let (repository, fingerprint) = owner.restore_request(&input, &operation).await?; + if let Some(receipt) = repository + .workspace_restore_receipt( + operation.operation, + fingerprint.clone(), + storage::Revision(input.checkpoint.revision), + ) + .await + .map_err(failure)? + { + return Ok(restored(receipt)); + } + if !input.accept_external_writers_risk { + return Err("agent_checkpoint_external_writers_not_fenced".into()); + } + let runs = owner + .runs + .clone() + .try_lock_owned() + .map_err(|_| "agent_checkpoint_busy")?; + if runs.closing + || !runs.controls.is_empty() + || !runs.preparing.is_empty() + || !runs.retained_cleanup.is_empty() + { + return Err("agent_checkpoint_busy".into()); + } + // A producer admitted before the runs gate may not have saved + // its Goal yet. Do not wait here: producers also need runs. + let goal_producer = owner + .goals + .gate + .try_lock() + .map_err(|_| "agent_checkpoint_busy")?; + let session = owner.session(&input.checkpoint.session_id).await?; + if owner.behavior.is_some() + || session.archived + || session.configuration.parent_session_id.is_some() + || !matches!(input.backend, BackendPreference::Model { .. }) + || session + .configuration + .backend + .as_ref() + .is_some_and(|v| !matches!(v, BackendPreference::Model { .. })) + || owner + .goals + .repository + .read(&session.id) + .await + .map_err(|e| e.to_string())? + .is_some() + { + return Err("agent_checkpoint_unsupported_session".into()); + } + if !owner.backend_available(&input.backend).await? { + return Err("agent_checkpoint_backend_unavailable".into()); + } + let root = owner.store.owned_root().map_err(failure)?; + let parent = workspace::observed( + owner.workspace_roots.clone(), + input.parent.clone(), + root.canonical_path().to_owned(), + ) + .await? + .ok_or("agent_directory_unavailable")?; + let request = storage::RestoreOperation { + operation: operation.operation, + fingerprint, + revision: storage::Revision(input.checkpoint.revision), + parent: parent.path.into(), + parent_identity: input.parent.directory_identity.clone(), + }; + let prepared = repository + .prepare_workspace_restore(request.clone()) + .await + .map_err(failure)?; + let root = owner.store.owned_root().map_err(failure)?; + let selection = workspace::selection_at( + owner.workspace_roots.clone(), + prepared.path.to_string_lossy().into_owned(), + root.canonical_path().to_owned(), + ) + .await? + .ok_or("agent_directory_unavailable")?; + let selected = workspace::initialize( + &owner.workspace_roots, + selection, + &owner.managed_worktrees, + root.canonical_path(), + root.control_directory(), + ) + .await? + .ok_or("agent_workspace_unavailable")?; + let binding = owner + .store + .attach_workspace(&owner.prefix, &selected) + .await + .map_err(failure)?; + let mut configuration = owner + .store + .get_session::(&session.id) + .await + .map_err(failure)? + .ok_or("agent_session_unavailable")? + .configuration; + configuration.workspace = binding; + configuration.permissions = input.permissions; + configuration.backend = Some(input.backend); + configuration.boundary_revision = session + .configuration + .boundary_revision + .checked_add(1) + .filter(|v| *v <= 9_007_199_254_740_991) + .ok_or("agent_permission_revision_exhausted")?; + let result = repository + .commit_workspace_restore( + request, + serde_json::to_value(configuration).map_err(|e| e.to_string())?, + now(), + ) + .await + .map(restored) + .map_err(failure); + drop(goal_producer); + drop(runs); + result + }) + }) + .await + } + pub async fn session_checkpoint_restore_receipt( + self: &Arc, + input: SessionCheckpointRestoreReceiptRead, + context: Context, + ) -> Result { + management(context.scope())?; + let scope = context.scope().clone(); + self.resource_work(&scope, move |owner, _| { + Box::pin(async move { + let (repository, fingerprint) = owner + .restore_request(&input.input, &input.reference) + .await?; + let id = input.reference.operation; + let operation = context.recover(input.reference)?.await?; + let restore = if operation.is_some() { + repository + .workspace_restore_receipt( + id, + fingerprint, + storage::Revision(input.input.checkpoint.revision), + ) + .await + .map_err(failure)? + .map(restored) + } else { + None + }; + Ok(SessionCheckpointRestoreReceipt { operation, restore }) + }) + }) + .await + } +} diff --git a/plugins/agent/native/src/owner/checkpoints/session.rs b/plugins/agent/native/src/owner/checkpoints/session.rs new file mode 100644 index 00000000000..d606a2ea14f --- /dev/null +++ b/plugins/agent/native/src/owner/checkpoints/session.rs @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//! Session history plus a caller-attested or explicitly unverified workspace copy. +use super::*; + +fn consistency(value: CheckpointWorkspaceConsistency) -> storage::WorkspaceConsistency { + match value { + CheckpointWorkspaceConsistency::CallerConfirmedQuiescent => { + storage::WorkspaceConsistency::CallerConfirmedQuiescent + } + CheckpointWorkspaceConsistency::Unverified => storage::WorkspaceConsistency::Unverified, + } +} +fn response( + value: storage::Publication, + mode: CheckpointWorkspaceConsistency, +) -> SessionCheckpointPublication { + SessionCheckpointPublication { + publication: publication(value), + workspace_consistency: mode, + warning: CheckpointWorkspaceWarning::ExternalWritersNotFenced, + } +} +fn requested(input: SessionCheckpointPublish) -> (Capture, CheckpointWorkspaceConsistency) { + let mut capture = capture(input.history); + capture.workspace = Some(storage::WorkspaceCapture { + root: Default::default(), + identity: String::new(), + consistency: consistency(input.workspace_consistency), + }); + (capture, input.workspace_consistency) +} + +impl Owner { + pub async fn publish_session_checkpoint( + self: &Arc, + input: SessionCheckpointPublish, + operation: &RecoveryRef, + scope: &Scope, + ) -> Result { + management(scope)?; + let operation = operation.clone(); + self.resource_work(scope, move |owner, _| { + Box::pin(async move { + let publisher = owner.history_checkpoints(); + let (mut capture, mode) = requested(input); + let request = publisher.request(&capture, &operation).map_err(failure)?; + let repository = publisher + .repository(&capture.session_id) + .await + .map_err(failure)?; + if let Some(original) = repository + .original_request(request) + .await + .map_err(failure)? + { + return repository + .publish(original) + .await + .map(|p| response(p, mode)) + .map_err(failure); + } + // Keep native admission closed until both history and workspace have been copied. + // Foreign Hosts and external applications remain covered only by the caller's assertion. + let runs = owner + .runs + .clone() + .try_lock_owned() + .map_err(|_| "agent_checkpoint_busy")?; + if runs.closing + || !runs.controls.is_empty() + || !runs.preparing.is_empty() + || !runs.retained_cleanup.is_empty() + { + return Err("agent_checkpoint_busy".into()); + } + let session = owner.session(&capture.session_id).await?; + if owner.behavior.is_some() + || session.configuration.parent_session_id.is_some() + || session + .configuration + .backend + .as_ref() + .is_some_and(|v| !matches!(v, BackendPreference::Model { .. })) + { + return Err("agent_checkpoint_unsupported_session".into()); + } + let inventory = owner + .store + .preview_bundle(&capture.session_id) + .await + .map_err(|e| e.to_string())?; + if inventory.sessions.len() != 1 { + return Err("agent_checkpoint_multiple_workspaces_unsupported".into()); + } + workspace::validate( + &owner.workspace_roots, + &session.configuration.workspace, + &owner.managed_worktrees, + ) + .await + .map_err(|_| "agent_workspace_unavailable")?; + capture.workspace = Some(storage::WorkspaceCapture { + root: session.configuration.workspace.path.into(), + identity: session.configuration.workspace.identity.into(), + consistency: consistency(mode), + }); + let result = publisher + .publish(capture, &operation) + .await + .map(|p| response(p, mode)) + .map_err(failure); + drop(runs); + result + }) + }) + .await + } + + pub async fn session_checkpoint_receipt( + self: &Arc, + input: SessionCheckpointReceiptRead, + context: Context, + ) -> Result { + management(context.scope())?; + let scope = context.scope().clone(); + self.resource_work(&scope, move |owner, _| { + Box::pin(async move { + let publisher = owner.history_checkpoints(); + let (capture, mode) = requested(input.input); + let request = publisher + .request(&capture, &input.reference) + .map_err(failure)?; + let operation = context.recover(input.reference)?.await?; + let checkpoint = if operation.is_some() { + let repository = publisher + .repository(&capture.session_id) + .await + .map_err(failure)?; + if let Some(original) = repository + .original_request(request) + .await + .map_err(failure)? + { + repository + .receipt(original) + .await + .map_err(failure)? + .map(|p| response(p, mode)) + } else { + None + } + } else { + None + }; + Ok(SessionCheckpointReceipt { + operation, + checkpoint, + }) + }) + }) + .await + } +} diff --git a/plugins/agent/native/src/owner/tests/checkpoints.rs b/plugins/agent/native/src/owner/tests/checkpoints.rs index 6ed718f7f6f..e81ee3a06c9 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints.rs @@ -148,6 +148,10 @@ async fn turn(f: &Fixture, listener: &TcpListener, session: &str, text: &str) { } #[path = "checkpoints/benchmark.rs"] mod benchmark; +#[path = "checkpoints/load.rs"] +mod load; +#[path = "checkpoints/workspace.rs"] +mod workspace; async fn close(f: &Fixture) { f.app.shutdown().await.unwrap(); diff --git a/plugins/agent/native/src/owner/tests/checkpoints/load.rs b/plugins/agent/native/src/owner/tests/checkpoints/load.rs new file mode 100644 index 00000000000..ee88098f4ad --- /dev/null +++ b/plugins/agent/native/src/owner/tests/checkpoints/load.rs @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use super::*; + +async fn load(f: &Fixture, input: Value, operation: Uuid) -> (RecoveryRef, Receipt) { + let client = f.app.client(); + let prepared = client + .prepare_mutation( + f.binding(2).await, + "loadHistoryCheckpoint".into(), + input, + scope(&f.issuer, true), + operation, + Attachments::new().into(), + ) + .await + .unwrap(); + let reference = prepared.reference().clone(); + let result = client.mutate_owned(prepared).await.unwrap().result().await; + (reference, result.receipt) +} +fn loaded(receipt: Receipt) -> Value { + let Receipt::Recorded { + outcome: OperationOutcome::Returned { value }, + .. + } = receipt + else { + panic!("{receipt:?}"); + }; + assert_eq!(value["state"]["kind"], "ready"); + value +} + +#[tokio::test] +async fn exact_load_reads_frozen_history_and_artifacts_without_changing_live_session() { + tokio::time::timeout(Duration::from_secs(90),async { + let listener=TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f=Fixture::new(&format!("http://{}/v1",listener.local_addr().unwrap()),None).await; + let session=f.create("Load checkpoint").await; + let bytes=vec![73;65539]; + attachments::upload(&f,&session,"before.txt",&bytes,Uuid::new_v4()).await.unwrap(); + turn(&f,&listener,&session,"frozen response").await; + let checkpoint=published(publish(&f,input(&f,&session,Value::Null).await,Uuid::new_v4()).await.1); + let head=checkpoint["state"]["head"].clone(); + turn(&f,&listener,&session,"later live response").await; + attachments::upload(&f,&session,"after.txt",b"only live",Uuid::new_v4()).await.unwrap(); + let original=json!({"sessionId":session,"revision":head["revision"]}); + let (reference,receipt)=load(&f,original.clone(),Uuid::new_v4()).await; + let result=loaded(receipt); + assert_eq!(result["head"],head); + let inspection=result["inspection"].clone(); + let page=f.query("loadedHistoryCheckpointHistory",json!({"inspection":inspection,"sessionId":session,"position":{"kind":"start"}})).await.unwrap(); + let text=page.to_string();assert!(text.contains("frozen response"));assert!(!text.contains("later live response")); + let live=f.query("history",json!({"sessionId":session,"position":{"kind":"tail"}})).await.unwrap(); + assert!(live.to_string().contains("later live response")); + let artifacts=f.query("loadedHistoryCheckpointArtifacts",json!({"inspection":inspection,"sessionId":session,"offset":0})).await.unwrap(); + assert_eq!(artifacts["artifacts"].as_array().unwrap().len(),1); + let artifact=&artifacts["artifacts"][0];assert_eq!(artifact["name"],"before.txt"); + let mut actual=Vec::new(); + while actual.len() (RecoveryRef, Value) { + let client = f.app.client(); + let prepared = client + .prepare_mutation( + f.binding(2).await, + method.into(), + input, + scope(&f.issuer, true), + id, + Attachments::new().into(), + ) + .await + .unwrap(); + let reference = prepared.reference().clone(); + let receipt = client + .mutate_owned(prepared) + .await + .unwrap() + .result() + .await + .receipt; + let Receipt::Recorded { + outcome: OperationOutcome::Returned { value }, + .. + } = receipt + else { + panic!("{receipt:?}") + }; + (reference, value) +} + +#[tokio::test] +async fn workspace_checkpoint_restores_same_session_and_runs_next_turn_then_cas() { + tokio::time::timeout(Duration::from_secs(120), async { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = Fixture::new(&format!("http://{}/v1", listener.local_addr().unwrap()), None).await; + let session = f.create("Restore workspace").await; + std::fs::write(f.workspace.join("answer.txt"), "checkpoint file").unwrap(); + turn(&f, &listener, &session, "checkpoint history").await; + let original = json!({"history":input(&f,&session,Value::Null).await,"workspaceConsistency":"unverified"}); + let (publication_ref,value) = mutate(&f,"publishSessionCheckpoint",original.clone(),Uuid::new_v4()).await; + assert_eq!(value["warning"],"external_writers_not_fenced"); + assert_eq!(value["workspaceConsistency"],"unverified"); + let head = value["publication"]["state"]["head"].clone(); + std::fs::write(f.workspace.join("answer.txt"), "source modified later").unwrap(); + let roots = f.query("directories", json!({"kind":"roots"})).await.unwrap(); + let restore_input = json!({"checkpoint":{"sessionId":session,"revision":head["revision"]}, + "parent":roots["roots"][1]["selection"],"permissions":{"access":"workspace_write","requestPermissions":true}, + "backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + let operation = Uuid::new_v4(); + let (restore_ref,result) = mutate(&f,"restoreSessionCheckpoint",restore_input.clone(),operation).await; + assert_eq!(result["sessionId"],session); + assert_eq!(result["head"],head); + assert_eq!(result["workspace"]["consistency"],"unverified"); + let target = std::path::PathBuf::from(result["path"].as_str().unwrap()); + assert_eq!(std::fs::read_to_string(target.join("answer.txt")).unwrap(),"checkpoint file"); + assert_eq!(std::fs::read_to_string(f.workspace.join("answer.txt")).unwrap(),"source modified later"); + let active = f.owner.session(&session).await.unwrap(); + assert_eq!(active.configuration.workspace.path,result["path"].as_str().unwrap()); + assert_eq!(f.mutation(2,"restoreSessionCheckpoint",restore_input.clone(),operation).await.unwrap(),result); + turn(&f,&listener,&session,"continued after workspace restore").await; + let history=f.query("history",json!({"sessionId":session,"position":{"kind":"tail"}})).await.unwrap().to_string(); + assert!(history.contains("checkpoint history") && history.contains("continued after workspace restore")); + let next = f.mutation(2,"publishSessionCheckpoint",json!({"history":input(&f,&session,head["revision"].clone()).await,"workspaceConsistency":"caller_confirmed_quiescent"}),Uuid::new_v4()).await.unwrap(); + assert_eq!(next["publication"]["state"]["kind"],"published", "{next}"); + assert_ne!(next["publication"]["state"]["head"]["revision"], head["revision"]); + assert_eq!(f.query("historyCheckpointHead",json!({"sessionId":session})).await.unwrap(),next["publication"]["state"]["head"]); + assert!(f.mutation(2,"restoreSessionCheckpoint",restore_input.clone(),Uuid::new_v4()).await.is_err()); + assert_eq!(f.query("sessionCheckpointReceipt",json!({"reference":publication_ref,"input":original})).await.unwrap()["checkpoint"],value); + let parent=target.parent().unwrap(); + std::fs::rename(parent,parent.with_file_name("moved-restored-parent")).unwrap(); + assert_eq!(f.query("sessionCheckpointRestoreReceipt",json!({"reference":restore_ref,"input":restore_input})).await.unwrap()["restore"],result); + close(&f).await; + }).await.unwrap(); +} + +#[tokio::test] +async fn workspace_checkpoint_requires_explicit_consistency_and_rejects_advanced_history_restore() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = Fixture::new( + &format!("http://{}/v1", listener.local_addr().unwrap()), + None, + ) + .await; + let session = f.create("No history rollback").await; + let history = input(&f, &session, Value::Null).await; + assert!( + f.mutation( + 2, + "publishSessionCheckpoint", + json!({"history":history}), + Uuid::new_v4() + ) + .await + .is_err() + ); + let value = f + .mutation( + 2, + "publishSessionCheckpoint", + json!({"history":history,"workspaceConsistency":"caller_confirmed_quiescent"}), + Uuid::new_v4(), + ) + .await + .unwrap(); + turn(&f, &listener, &session, "newer history").await; + let roots = f + .query("directories", json!({"kind":"roots"})) + .await + .unwrap(); + let request = json!({"checkpoint":{"sessionId":session,"revision":value["publication"]["state"]["head"]["revision"]},"parent":roots["roots"][1]["selection"],"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + assert!( + f.mutation(2, "restoreSessionCheckpoint", request, Uuid::new_v4()) + .await + .is_err() + ); + assert_eq!( + f.owner + .session(&session) + .await + .unwrap() + .configuration + .workspace + .path, + f.workspace.to_str().unwrap() + ); + close(&f).await; +} + +#[tokio::test] +async fn workspace_restore_rejects_an_inflight_goal_producer() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = Fixture::new( + &format!("http://{}/v1", listener.local_addr().unwrap()), + None, + ) + .await; + let session = f.create("Goal producer during restore").await; + let value = f + .mutation( + 2, + "publishSessionCheckpoint", + json!({ + "history":input(&f,&session,Value::Null).await, + "workspaceConsistency":"unverified" + }), + Uuid::new_v4(), + ) + .await + .unwrap(); + let roots = f + .query("directories", json!({"kind":"roots"})) + .await + .unwrap(); + let request = json!({"checkpoint":{"sessionId":session,"revision":value["publication"]["state"]["head"]["revision"]},"parent":roots["roots"][1]["selection"],"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + // A Goal producer can be admitted before restore takes the native runs gate, + // yet not have written its first durable record. Its own gate must exclude restore. + let producer = f.owner.goals.gate.lock().await; + let result = tokio::time::timeout( + Duration::from_secs(10), + f.mutation( + 2, + "restoreSessionCheckpoint", + request.clone(), + Uuid::new_v4(), + ), + ) + .await + .unwrap(); + assert!( + result.is_err(), + "restore passed an unfinished Goal producer: {result:?}" + ); + assert_eq!( + f.owner + .session(&session) + .await + .unwrap() + .configuration + .workspace + .path, + f.workspace.to_str().unwrap() + ); + drop(producer); + f.mutation(2, "restoreSessionCheckpoint", request, Uuid::new_v4()) + .await + .unwrap(); + close(&f).await; +} diff --git a/plugins/agent/native/src/service.rs b/plugins/agent/native/src/service.rs index 04e34edd96a..b2406e071fe 100644 --- a/plugins/agent/native/src/service.rs +++ b/plugins/agent/native/src/service.rs @@ -390,6 +390,60 @@ impl Handler for Agent { "readHistoryCheckpoint" => { owner.read_history_checkpoint(decode(input)?, context).await } + "loadHistoryCheckpoint" => { + let operation = context.operation().ok_or("agent_mutation_required")?; + encode( + owner + .load_history_checkpoint(decode(input)?, operation, context.scope()) + .await?, + ) + } + "restoreSessionCheckpoint" => { + let operation = context.operation().ok_or("agent_mutation_required")?; + encode( + owner + .restore_session_checkpoint(decode(input)?, operation, context.scope()) + .await?, + ) + } + "sessionCheckpointRestoreReceipt" => encode( + owner + .session_checkpoint_restore_receipt(decode(input)?, context) + .await?, + ), + "publishSessionCheckpoint" => { + let operation = context.operation().ok_or("agent_mutation_required")?; + encode( + owner + .publish_session_checkpoint(decode(input)?, operation, context.scope()) + .await?, + ) + } + "sessionCheckpointReceipt" => encode( + owner + .session_checkpoint_receipt(decode(input)?, context) + .await?, + ), + "historyCheckpointLoadReceipt" => encode( + owner + .history_checkpoint_load_receipt(decode(input)?, context) + .await?, + ), + "loadedHistoryCheckpointHistory" => checkpoint_output( + owner + .loaded_checkpoint_history(decode(input)?, context.scope()) + .await?, + ), + "loadedHistoryCheckpointArtifacts" => checkpoint_output( + owner + .loaded_checkpoint_artifacts(decode(input)?, context.scope()) + .await?, + ), + "loadedHistoryCheckpointArtifact" => checkpoint_output( + owner + .loaded_checkpoint_artifact(decode(input)?, context.scope()) + .await?, + ), "fork" => { let operation = context.operation().ok_or("agent_mutation_required")?; encode( @@ -570,7 +624,6 @@ impl Handler for Agent { Ok(result) } "history" => { - use maka_agent_ports::session::history::Page; use maka_agent_ports::session::history::Read; let input: History = decode(input)?; owner.require_session(&input.session_id).await?; @@ -592,13 +645,8 @@ impl Handler for Agent { } .validate() .map_err(|_| "agent_cursor_invalid")?; - let result = { - let Page::Ready { - through, - chunks, - before, - after, - } = owner + let result = HistoryPage::from( + owner .scoped_store(context.scope(), &input.session_id) .await? .history_view( @@ -616,46 +664,9 @@ impl Handler for Agent { }, ) .await - .map_err(|_| "agent_history_unavailable")?; - HistoryPage::Ready { - through, - before, - after, - chunks: chunks - .into_iter() - .map(|chunk| HistoryChunk { - model_part: chunk.model_part, - has_references: chunk.has_references, - final_output: chunk.final_output, - model: chunk.model, - origin: chunk.origin, - compaction: chunk.compaction, - attachments: chunk - .attachments - .into_iter() - .map(|attachment| HistoryAttachment { - name: attachment.name, - mime_type: attachment.mime_type, - bytes: attachment.bytes, - kind: attachment.kind, - }) - .collect(), - timing: chunk.timing, - field: chunk.field, - message_id: chunk.message_id, - turn_id: chunk.turn_id, - timestamp: chunk.timestamp, - role: chunk.role, - order: chunk.order, - position: chunk.position, - offset: chunk.offset, - total_bytes: chunk.total_bytes, - text: chunk.text, - tool: chunk.tool, - }) - .collect(), - } - }; + .map_err(|_| "agent_history_unavailable")?, + ); + owner.require_session(&input.session_id).await?; if context.scope().cancellation.is_cancelled() { return Err("agent_observation_cancelled".into()); @@ -1363,6 +1374,12 @@ async fn catalog_page(owner: &Owner, input: &List) -> Result(input: Value) -> Result { serde_json::from_value(input).map_err(|_| "agent_input_invalid".into()) } +fn checkpoint_output(value: T) -> Result { + let value = encode(value)?; + maka_plugin_core::json::exact_numbers(&value) + .map_err(|_| "agent_history_checkpoint_output_unavailable")?; + Ok(value) +} fn encode(value: T) -> Result { serde_json::to_value(value).map_err(|_| "agent_output_unavailable".into()) } diff --git a/products/maka/cli/src/acp/tests/public_client.rs b/products/maka/cli/src/acp/tests/public_client.rs index 568b7d6e6f8..c144fa1cfdd 100644 --- a/products/maka/cli/src/acp/tests/public_client.rs +++ b/products/maka/cli/src/acp/tests/public_client.rs @@ -162,6 +162,19 @@ async fn published_product_endpoint_exposes_history_checkpoint_and_original_rece assert!(ended); let mut stage=maka_event_log::bundle::StagedBundle::read(bytes.as_slice()).await.unwrap(); stage.validate_history().await.unwrap(); + stage.close().await.unwrap(); + let load_input=json!({"sessionId":fixture.session,"revision":head["revision"]}); + let mut loading=fixture.native.call("agent","loadHistoryCheckpoint".into(),load_input.clone(),Default::default()).await.unwrap(); + loading.start().await.unwrap(); + let Reply::Prepared {reference:load_ref}=loading.receive().await.unwrap() else {panic!("load not prepared")}; + let Reply::Mutation {reply,cleanup}=loading.confirm().await.unwrap() else {panic!("load not returned")}; + cleanup.unwrap(); + let Receipt::Recorded {outcome:OperationOutcome::Returned {value:loaded},..}=reply.receipt else {panic!("load failed")}; + assert_eq!(loaded["state"]["kind"],"ready");loading.close().await.unwrap(); + let observed=support::query(&fixture.native,"agent","historyCheckpointLoadReceipt",json!({"reference":load_ref,"input":load_input})).await; + assert_eq!(observed["load"],loaded); + let history=support::query(&fixture.native,"agent","loadedHistoryCheckpointHistory",json!({"inspection":loaded["inspection"],"sessionId":fixture.session,"position":{"kind":"start"}})).await; + assert_eq!(history["kind"],"ready"); assert!(fixture.model_requests.try_recv().is_err(),"checkpoint management never runs the model"); stream.close().await.unwrap(); fixture.native.close().await.unwrap(); fixture.client.quit().await.unwrap(); From c3fd38545440733b852815c235d7930f9bcd6061 Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Wed, 4 Nov 2026 20:58:07 +0800 Subject: [PATCH 3/7] fix(storage): sync checkpoint directories with readable handles cap-std can open directories using O_PATH on Linux. Passing those handles directly to fsync makes workspace checkpoint loading and restoration fail with EBADF, even though the same tests pass on macOS. Reopen "." relative to the owned directory before syncing both the copied workspace tree and its restore parent. Preserve directory authority and propagate sync failures before recording a completed preparation. The existing checkpoint and Host regression tests cover both paths. Refs #6629 Generated-by: Codex --- platform/event-log/src/root_store/checkpoints/restore.rs | 2 +- platform/event-log/src/root_store/checkpoints/workspace.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/platform/event-log/src/root_store/checkpoints/restore.rs b/platform/event-log/src/root_store/checkpoints/restore.rs index 86361f9e0a6..729f0fe4a3a 100644 --- a/platform/event-log/src/root_store/checkpoints/restore.rs +++ b/platform/event-log/src/root_store/checkpoints/restore.rs @@ -185,7 +185,7 @@ impl HistoryCheckpoints { let destination = directory.open_dir_nofollow(name)?; let identity = directory_identity(&destination)?; workspace::materialize_in(&objects, &prepared.workspace, &destination)?; - directory.into_std_file().sync_all()?; + directory.open(".")?.sync_all()?; verify_destination(&objects, &prepared, &request.operation, &identity)?; let key = request.key()?; let identity = serde_json::to_string(&identity)?; diff --git a/platform/event-log/src/root_store/checkpoints/workspace.rs b/platform/event-log/src/root_store/checkpoints/workspace.rs index 0964d3a4b33..bcc256dfce5 100644 --- a/platform/event-log/src/root_store/checkpoints/workspace.rs +++ b/platform/event-log/src/root_store/checkpoints/workspace.rs @@ -396,7 +396,9 @@ pub(super) fn materialize_in( sync(&dir.open_dir_nofollow(entry.file_name())?)?; } } - dir.try_clone()?.into_std_file().sync_all()?; + // cap-std directory handles can be O_PATH on Linux; reopen the same + // directory for I/O rather than passing a path-only descriptor to fsync. + dir.open(".")?.sync_all()?; Ok(()) } sync(dir) From 7e24b966d9184d22ae9470c0a80af9acbedc8cee Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Thu, 5 Nov 2026 19:16:01 +0800 Subject: [PATCH 4/7] fix(agent): address checkpoint restoration and publication review Validate the live history cut and idle state instead of captured metadata revisions. Scope capture admission to the Session and overlapping workspaces, stream immutable inspection reads, honor project ignore rules, and unify checkpoint publication APIs. Generated-by: Codex --- Cargo.lock | 1 + packages/plugin-sdk/src/contracts.ts | 10325 ++++++++-------- packages/plugin-sdk/src/generated/methods.ts | 611 +- platform/event-log/Cargo.toml | 1 + platform/event-log/src/bundle.rs | 1 + platform/event-log/src/bundle/export.rs | 23 +- platform/event-log/src/bundle/stage.rs | 18 + platform/event-log/src/checkpoints.rs | 41 +- platform/event-log/src/checkpoints/load.rs | 2 +- platform/event-log/src/checkpoints/restore.rs | 1 + platform/event-log/src/lib.rs | 35 + .../event-log/src/root_store/checkpoints.rs | 112 +- .../src/root_store/checkpoints/load.rs | 98 +- .../src/root_store/checkpoints/objects.rs | 80 +- .../src/root_store/checkpoints/restore.rs | 28 +- .../src/root_store/checkpoints/tests/load.rs | 57 + .../root_store/checkpoints/tests/workspace.rs | 230 +- .../src/root_store/checkpoints/workspace.rs | 126 +- plugins/agent/api/README.md | 32 +- plugins/agent/api/README.zh-CN.md | 32 +- plugins/agent/api/src/contract.rs | 12 +- plugins/agent/api/src/contract/checkpoints.rs | 97 +- plugins/agent/native/README.md | 2 +- plugins/agent/native/README.zh-CN.md | 2 +- plugins/agent/native/src/checkpoints.rs | 15 +- plugins/agent/native/src/owner.rs | 37 +- plugins/agent/native/src/owner/checkpoints.rs | 178 +- .../native/src/owner/checkpoints/load.rs | 42 +- .../native/src/owner/checkpoints/restore.rs | 50 +- .../native/src/owner/checkpoints/session.rs | 72 +- plugins/agent/native/src/owner/goal.rs | 4 + plugins/agent/native/src/owner/projects.rs | 12 + plugins/agent/native/src/owner/runs.rs | 64 +- .../native/src/owner/tests/checkpoints.rs | 30 +- .../src/owner/tests/checkpoints/benchmark.rs | 2 +- .../src/owner/tests/checkpoints/load.rs | 14 +- .../src/owner/tests/checkpoints/workspace.rs | 122 +- plugins/agent/native/src/owner/title.rs | 5 + plugins/agent/native/src/service.rs | 33 +- .../maka/cli/src/acp/tests/public_client.rs | 8 +- 40 files changed, 6095 insertions(+), 6560 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9200198eb0..41956b8b7cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7878,6 +7878,7 @@ dependencies = [ "cap-std", "futures-util", "hex", + "ignore", "libc", "maka-agent-facts", "maka-agent-ports", diff --git a/packages/plugin-sdk/src/contracts.ts b/packages/plugin-sdk/src/contracts.ts index 7f997aaf0e5..db6a0854f28 100644 --- a/packages/plugin-sdk/src/contracts.ts +++ b/packages/plugin-sdk/src/contracts.ts @@ -1946,1133 +1946,1307 @@ export const contracts = { }, resources: {}, }, - compact: { - mode: 'mutation', + checkpointReceipt: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCompact', + title: 'AgentCheckpointReceiptRead', type: 'object', properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - model: { - $ref: '#/$defs/ModelSelection', - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + reference: { + $ref: '#/$defs/RecoveryRef', }, - prompt: { + input: { description: - "One invocation only; omission uses the Session's effective preference.", - type: ['string', 'null'], + 'The exact original input. Changed input cannot select another publication.', + $ref: '#/$defs/AgentCheckpointPublish', }, }, additionalProperties: false, - required: ['sessionId', 'model'], + required: ['reference', 'input'], $defs: { - ModelSelection: { - description: - 'Stable model identity, resolved from current connection settings at admission.', + RecoveryRef: { type: 'object', properties: { - connectionId: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { type: 'string', - minLength: 1, }, - modelId: { + method: { type: 'string', - minLength: 1, + }, + operation: { + type: 'string', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, - required: ['connectionId', 'modelId'], - }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + additionalProperties: false, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCompactResult', - oneOf: [ - { + DeploymentRef: { type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'started', }, - execution: { - $ref: '#/$defs/AgentExecution', + deployment: { + type: 'string', + format: 'uuid', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'execution'], + required: ['stateDomain', 'deployment', 'revision'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - kind: { + name: { type: 'string', - const: 'rejected', }, - reason: { - $ref: '#/$defs/AgentCompactRejection', + version: { + type: 'integer', + format: 'uint32', + minimum: 0, + }, + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'reason'], + required: ['name', 'version', 'transport', 'digest'], }, - ], - $defs: { - AgentExecution: { - description: - 'Canonical physical execution receipt, independent of the current Session tip.', + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + AgentCheckpointPublish: { type: 'object', properties: { sessionId: { type: 'string', + minLength: 1, }, - turnId: { - type: 'string', - }, - runId: { - type: 'string', + expectedRevision: { + type: ['string', 'null'], + format: 'uuid', }, - invocationId: { + confirmedSubtreeDigest: { + description: + 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', type: 'string', + pattern: '^[a-f0-9]{64}$', }, - highWater: { - description: 'Run-local event count, not the root-global transcript cursor.', - type: 'integer', - format: 'uint64', - minimum: 0, - }, - state: { - $ref: '#/$defs/AgentExecutionState', + workspaceConsistency: { + description: + 'Null captures history only. Otherwise the caller explicitly selects the\nWorkspace consistency guarantee; other applications are never fenced.', + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], - }, - AgentExecutionState: { - type: 'string', - enum: [ - 'admitted', - 'running', - 'waiting', - 'paused', - 'completed', - 'failed', - 'cancelled', + required: [ + 'sessionId', + 'expectedRevision', + 'confirmedSubtreeDigest', + 'workspaceConsistency', ], }, - AgentCompactRejection: { - type: 'string', - enum: ['busy', 'closed', 'archived', 'backend_unavailable', 'workspace_changed'], - }, - }, - }, - resources: {}, - }, - compactionPrompt: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', + }, + ], }, }, - additionalProperties: false, - required: ['sessionId'], }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCompactionPrompt', + title: 'AgentCheckpointReceipt', type: 'object', properties: { - revision: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - prompt: { - type: ['string', 'null'], - }, - compactionPrompt: { - type: 'string', + operation: { + description: + 'Original Host observation. Domain success never fabricates Host cleanup confirmation.', + anyOf: [ + { + $ref: '#/$defs/OperationObservation', + }, + { + type: 'null', + }, + ], }, - defaultCompactionPrompt: { - type: 'string', + publication: { + description: + 'Authoritative domain result even when the Host response was lost.\nAbsence is not permission to recapture under a replacement operation.', + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointPublication', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['revision', 'prompt', 'compactionPrompt', 'defaultCompactionPrompt'], - }, - resources: {}, - }, - computerApps: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentComputerApps', - type: 'object', - properties: { - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentComputerAppActivityState', - }, - }, - }, - additionalProperties: false, - required: ['entries'], $defs: { - AgentComputerAppActivityState: { + OperationObservation: { type: 'object', properties: { - activity: { - $ref: '#/$defs/ComputerAppActivity', + reference: { + $ref: '#/$defs/RecoveryRef', }, - stopped: { - type: 'boolean', + outcome: { + $ref: '#/$defs/OperationOutcome', + }, + cleanup: { + $ref: '#/$defs/OperationCleanup', }, }, additionalProperties: false, - required: ['activity', 'stopped'], + required: ['reference', 'outcome', 'cleanup'], }, - ComputerAppActivity: { + RecoveryRef: { type: 'object', properties: { - invocation: { - $ref: '#/$defs/Invocation', - }, - operationId: { - type: 'string', - }, - target: { - $ref: '#/$defs/GrantTarget', + source: { + $ref: '#/$defs/DeploymentRef', }, - display: { - $ref: '#/$defs/ComputerAppDisplay', + namespace: { + $ref: '#/$defs/InstanceId', }, - }, - additionalProperties: false, - required: ['invocation', 'operationId', 'target', 'display'], - }, - Invocation: { - type: 'object', - properties: { - session_id: { + export: { type: 'string', }, - turn_id: { + method: { type: 'string', }, - run_id: { + operation: { type: 'string', + format: 'uuid', }, - invocation_id: { + fingerprint: { type: 'string', }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', + }, }, - required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], + additionalProperties: false, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - GrantTarget: { + DeploymentRef: { type: 'object', properties: { - providerId: { - type: 'string', - }, - contractId: { - type: 'string', - }, - serverId: { + stateDomain: { type: 'string', }, - toolName: { + deployment: { type: 'string', + format: 'uuid', }, - capability: { - $ref: '#/$defs/GrantCapability', - }, - scope: { - $ref: '#/$defs/GrantScope', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, - required: [ - 'providerId', - 'contractId', - 'serverId', - 'toolName', - 'capability', - 'scope', - ], + additionalProperties: false, + required: ['stateDomain', 'deployment', 'revision'], }, - GrantCapability: { - oneOf: [ - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + type: 'object', + properties: { + name: { type: 'string', - enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], }, - { - description: - 'File download consent for a browser origin, independent of page access.', - type: 'string', - const: 'browser_download', + version: { + type: 'integer', + format: 'uint32', + minimum: 0, }, - { - description: - "Read the browser profile's browsing history, independent of page access.", - type: 'string', - const: 'browser_history', + transport: { + $ref: '#/$defs/Transport', }, - { - description: - 'File upload consent for a browser origin, independent of reads and downloads.', + digest: { type: 'string', - const: 'browser_upload', }, - ], + }, + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], }, - GrantScope: { + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + OperationOutcome: { + description: + 'Unresolved means no durable result is known, including after process loss.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'browser_origin', - }, - origin: { - type: 'string', + const: 'unresolved', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'origin'], }, { - description: - 'One selected browser profile generation; never an origin or app-wide grant.', type: 'object', properties: { kind: { type: 'string', - const: 'browser_profile', + const: 'returned', }, - browserId: { + value: true, + }, + additionalProperties: false, + required: ['kind', 'value'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'unknown', }, - generation: { + diagnostic: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'browserId', 'generation'], + required: ['kind', 'diagnostic'], }, + ], + }, + OperationCleanup: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'capability', + const: 'unconfirmed', }, }, - additionalProperties: false, required: ['kind'], + additionalProperties: false, }, { type: 'object', properties: { kind: { type: 'string', - const: 'mcp_tool', - }, - serverId: { - type: 'string', - }, - toolName: { - type: 'string', + const: 'confirmed', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'serverId', 'toolName'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'computer_app', - }, - platform: { - $ref: '#/$defs/ComputerAppPlatform', + const: 'failed', }, - applicationId: { + diagnostic: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'platform', 'applicationId'], + required: ['kind', 'diagnostic'], }, ], }, - ComputerAppPlatform: { - type: 'string', - enum: ['macos', 'windows'], - }, - ComputerAppDisplay: { + AgentCheckpointPublication: { type: 'object', properties: { - name: { - type: 'string', + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', }, - appPath: { - type: 'string', + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', }, - risk: { + workspace: { anyOf: [ { - $ref: '#/$defs/ComputerAppRisk', + $ref: '#/$defs/AgentCheckpointWorkspaceNotice', }, { type: 'null', }, ], }, - warning: { - type: ['string', 'null'], + }, + additionalProperties: false, + required: ['state', 'cleanupPending'], + }, + AgentHistoryCheckpointState: { + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'capturing', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'prepared', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'published', + }, + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + }, + additionalProperties: false, + required: ['kind', 'head'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'conflict', + }, + actual: { + type: ['string', 'null'], + format: 'uuid', + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'interrupted', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'unavailable', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + ], + }, + AgentHistoryCheckpointHead: { + type: 'object', + properties: { + revision: { + type: 'string', + format: 'uuid', + }, + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['name', 'appPath'], + required: ['revision', 'manifest'], }, - ComputerAppRisk: { - type: 'string', - enum: ['low', 'high'], + AgentHistoryCheckpointObject: { + type: 'object', + properties: { + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', + }, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['digest', 'bytes'], + }, + AgentCheckpointWorkspaceNotice: { + type: 'object', + properties: { + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + }, + additionalProperties: false, + required: ['consistency', 'warning'], + }, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', + }, + ], + }, + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', + type: 'string', + const: 'external_writers_not_fenced', + }, + ], }, }, }, resources: {}, }, - context: { - mode: 'query', + compact: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentContextRead', + title: 'AgentCompact', type: 'object', properties: { sessionId: { type: 'string', + minLength: 1, }, - through: { - description: - 'Null captures the committed head; a supplied fence must not exceed it.', - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['sessionId', 'through'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentContextReport', - type: 'object', - properties: { - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + model: { + $ref: '#/$defs/ModelSelection', }, - request: { - description: - 'Latest Main request made by this Session, excluding copied source requests.', + thinkingLevel: { anyOf: [ { - $ref: '#/$defs/AgentContextRequest', + $ref: '#/$defs/ThinkingLevel', }, { type: 'null', }, ], }, + prompt: { + description: + "One invocation only; omission uses the Session's effective preference.", + type: ['string', 'null'], + }, }, additionalProperties: false, - required: ['through', 'request'], + required: ['sessionId', 'model'], $defs: { - AgentContextRequest: { + ModelSelection: { + description: + 'Stable model identity, resolved from current connection settings at admission.', type: 'object', properties: { - invocationId: { + connectionId: { type: 'string', + minLength: 1, }, - stepId: { + modelId: { type: 'string', + minLength: 1, }, - sequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - recordedAt: { - description: 'Original request timestamp in milliseconds since the Unix epoch.', - type: 'number', - format: 'double', - minimum: 0, - maximum: 9007199254740991, + }, + required: ['connectionId', 'modelId'], + }, + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCompactResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'started', }, - sourceThrough: { - description: 'Original input history fence, before this request was recorded.', - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + execution: { + $ref: '#/$defs/AgentExecution', }, - modelId: { + }, + additionalProperties: false, + required: ['kind', 'execution'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'rejected', }, - contextWindow: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - inputLimit: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - state: { - $ref: '#/$defs/AgentContextRequestState', - }, - breakdown: { - description: - 'Original Host prepared-input measurements, not provider wire sizes or exact tokenization.', - anyOf: [ - { - $ref: '#/$defs/PreparedContextBreakdown', - }, - { - type: 'null', - }, - ], - }, - reportedInputTokens: { - description: - 'Provider-reported input tokens from this exact completed step, if available.', - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + reason: { + $ref: '#/$defs/AgentCompactRejection', }, }, additionalProperties: false, - required: [ - 'invocationId', - 'stepId', - 'sequence', - 'recordedAt', - 'sourceThrough', - 'modelId', - 'contextWindow', - 'inputLimit', - 'state', - 'breakdown', - 'reportedInputTokens', - ], - }, - AgentContextRequestState: { - type: 'string', - enum: ['requested', 'completed', 'interrupted'], + required: ['kind', 'reason'], }, - PreparedContextBreakdown: { + ], + $defs: { + AgentExecution: { description: - 'Host-prepared adapter input, not provider tokenization or HTTP wire bytes.', + 'Canonical physical execution receipt, independent of the current Session tip.', type: 'object', properties: { - system: { - $ref: '#/$defs/ContextBucket', - }, - user: { - $ref: '#/$defs/ContextBucket', - }, - assistant: { - $ref: '#/$defs/ContextBucket', + sessionId: { + type: 'string', }, - tool: { - description: - 'Messages with the tool role; assistant-embedded results stay in assistant.', - $ref: '#/$defs/ContextBucket', + turnId: { + type: 'string', }, - toolSchemas: { - $ref: '#/$defs/ContextBucket', + runId: { + type: 'string', }, - counts: { - $ref: '#/$defs/ContextCounts', + invocationId: { + type: 'string', }, - }, - additionalProperties: false, - required: ['system', 'user', 'assistant', 'tool', 'toolSchemas', 'counts'], - }, - ContextBucket: { - type: 'object', - properties: { - contentBytes: { - description: - 'UTF-8 text and compact JSON payload bytes counted by the Host heuristic.\nExcludes file encodings, message metadata, request-level provider options\nand synthetic framing overhead. Tool definitions are counted in full.', + highWater: { + description: 'Run-local event count, not the root-global transcript cursor.', type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, }, - estimatedTokens: { - description: - 'Heuristic: separately rounded text/JSON bytes / 3, plus message/tool\nframing and 1024 per media part. Never provider-measured tokens.', - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + state: { + $ref: '#/$defs/AgentExecutionState', }, }, additionalProperties: false, - required: ['contentBytes', 'estimatedTokens'], + required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], }, - ContextCounts: { - type: 'object', - properties: { - messages: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - media: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - toolCalls: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - toolResults: { - description: 'Result parts across both tool and assistant roles.', - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - tools: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['messages', 'media', 'toolCalls', 'toolResults', 'tools'], + AgentExecutionState: { + type: 'string', + enum: [ + 'admitted', + 'running', + 'waiting', + 'paused', + 'completed', + 'failed', + 'cancelled', + ], + }, + AgentCompactRejection: { + type: 'string', + enum: ['busy', 'closed', 'archived', 'backend_unavailable', 'workspace_changed'], }, }, }, resources: {}, }, - create: { - mode: 'mutation', + compactionPrompt: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCreate', + title: 'AgentSessionRead', type: 'object', properties: { - backend: { - description: 'Initial next-turn preference; admission resolves it again.', - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], + sessionId: { + type: 'string', + minLength: 1, }, - automaticTitle: { - description: - 'Treat title as a placeholder for one title derived from the first user message.', - type: 'boolean', - default: false, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCompactionPrompt', + type: 'object', + properties: { + revision: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - tools: { - description: - "Immutable narrowing of this Agent's installed tools. None uses the installation;\nan empty list permits no model tools. External executors cannot enforce this.", - type: ['array', 'null'], - items: { - type: 'string', - }, - default: null, - uniqueItems: true, + prompt: { + type: ['string', 'null'], }, - title: { - description: - 'A concise, specific task name people can recognize in navigation; avoid generic worker numbers.', + compactionPrompt: { + type: 'string', + }, + defaultCompactionPrompt: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['revision', 'prompt', 'compactionPrompt', 'defaultCompactionPrompt'], + }, + resources: {}, + }, + computerApps: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionRead', + type: 'object', + properties: { + sessionId: { type: 'string', minLength: 1, }, - labels: { + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentComputerApps', + type: 'object', + properties: { + entries: { type: 'array', items: { - type: 'string', + $ref: '#/$defs/AgentComputerAppActivityState', }, }, - workspace: { - $ref: '#/$defs/AgentWorkspaceSelection', - }, }, additionalProperties: false, - required: ['title', 'labels', 'workspace'], + required: ['entries'], $defs: { - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], + AgentComputerAppActivityState: { + type: 'object', + properties: { + activity: { + $ref: '#/$defs/ComputerAppActivity', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', - }, - }, - additionalProperties: false, - required: ['kind', 'selection'], + stopped: { + type: 'boolean', }, - ], - }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + additionalProperties: false, + required: ['activity', 'stopped'], }, - AgentExecutorSelection: { + ComputerAppActivity: { type: 'object', properties: { - executor: { + invocation: { + $ref: '#/$defs/Invocation', + }, + operationId: { type: 'string', - minLength: 1, }, - settings: { - $ref: '#/$defs/ExecutorSettings', + target: { + $ref: '#/$defs/GrantTarget', + }, + display: { + $ref: '#/$defs/ComputerAppDisplay', }, }, additionalProperties: false, - required: ['executor', 'settings'], + required: ['invocation', 'operationId', 'target', 'display'], }, - ExecutorSettings: { - description: - 'Executor-owned model selection, independent of Host model connections.', + Invocation: { type: 'object', properties: { - model: { - type: ['string', 'null'], + session_id: { + type: 'string', }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + turn_id: { + type: 'string', }, - runtime: { - type: ['string', 'null'], + run_id: { + type: 'string', + }, + invocation_id: { + type: 'string', }, }, - additionalProperties: false, + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', + GrantTarget: { + type: 'object', + properties: { + providerId: { + type: 'string', + }, + contractId: { + type: 'string', + }, + serverId: { + type: 'string', + }, + toolName: { + type: 'string', + }, + capability: { + $ref: '#/$defs/GrantCapability', + }, + scope: { + $ref: '#/$defs/GrantScope', + }, + }, + required: [ + 'providerId', + 'contractId', + 'serverId', + 'toolName', + 'capability', + 'scope', + ], }, - AgentWorkspaceSelection: { + GrantCapability: { + oneOf: [ + { + type: 'string', + enum: ['browser', 'computer_use', 'computer_audio', 'desktop_mcp', 'mcp'], + }, + { + description: + 'File download consent for a browser origin, independent of page access.', + type: 'string', + const: 'browser_download', + }, + { + description: + "Read the browser profile's browsing history, independent of page access.", + type: 'string', + const: 'browser_history', + }, + { + description: + 'File upload consent for a browser origin, independent of reads and downloads.', + type: 'string', + const: 'browser_upload', + }, + ], + }, + GrantScope: { oneOf: [ { - description: 'A Git worktree of the directory at `path`, managed by the Host.', type: 'object', properties: { kind: { type: 'string', - const: 'isolated_git', + const: 'browser_origin', }, - path: { + origin: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'origin'], }, { - description: 'A registered Project at one of its locations.', + description: + 'One selected browser profile generation; never an origin or app-wide grant.', type: 'object', properties: { kind: { type: 'string', - const: 'project', + const: 'browser_profile', }, - project: { - $ref: '#/$defs/AgentProjectRef', + browserId: { + type: 'string', }, - path: { + generation: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'project', 'path'], + required: ['kind', 'browserId', 'generation'], }, { - description: - 'The directory at `path`: absolute, `~` standing for the home directory,\nwithin the configured workspace locations.', type: 'object', properties: { kind: { type: 'string', - const: 'directory', + const: 'capability', }, - path: { + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'mcp_tool', + }, + serverId: { + type: 'string', + }, + toolName: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'path'], + required: ['kind', 'serverId', 'toolName'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'session', + const: 'computer_app', }, - sessionId: { + platform: { + $ref: '#/$defs/ComputerAppPlatform', + }, + applicationId: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'sessionId'], + required: ['kind', 'platform', 'applicationId'], }, ], }, - AgentProjectRef: { + ComputerAppPlatform: { + type: 'string', + enum: ['macos', 'windows'], + }, + ComputerAppDisplay: { type: 'object', properties: { - id: { + name: { type: 'string', - format: 'uuid', }, - revision: { + appPath: { type: 'string', - pattern: '^[a-f0-9]{64}$', + }, + risk: { + anyOf: [ + { + $ref: '#/$defs/ComputerAppRisk', + }, + { + type: 'null', + }, + ], + }, + warning: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['id', 'revision'], + required: ['name', 'appPath'], + }, + ComputerAppRisk: { + type: 'string', + enum: ['low', 'high'], }, }, }, - output: { + resources: {}, + }, + context: { + mode: 'query', + input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCreated', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'created', + title: 'AgentContextRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + through: { + description: + 'Null captures the committed head; a supplied fence must not exceed it.', + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sessionId', 'through'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentContextReport', + type: 'object', + properties: { + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + request: { + description: + 'Latest Main request made by this Session, excluding copied source requests.', + anyOf: [ + { + $ref: '#/$defs/AgentContextRequest', }, - session: { - $ref: '#/$defs/AgentSession', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'session'], + ], }, - { + }, + additionalProperties: false, + required: ['through', 'request'], + $defs: { + AgentContextRequest: { type: 'object', properties: { - kind: { + invocationId: { type: 'string', - const: 'rejected', }, - reason: { - $ref: '#/$defs/AgentRejection', + stepId: { + type: 'string', }, - }, - additionalProperties: false, - required: ['kind', 'reason'], - }, - ], - $defs: { - AgentSession: { - type: 'object', - properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], + sequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], + recordedAt: { + description: 'Original request timestamp in milliseconds since the Unix epoch.', + type: 'number', + format: 'double', + minimum: 0, + maximum: 9007199254740991, }, - backend: { + sourceThrough: { + description: 'Original input history fence, before this request was recorded.', + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + modelId: { + type: 'string', + }, + contextWindow: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + inputLimit: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + state: { + $ref: '#/$defs/AgentContextRequestState', + }, + breakdown: { + description: + 'Original Host prepared-input measurements, not provider wire sizes or exact tokenization.', anyOf: [ { - $ref: '#/$defs/AgentBackendPreference', + $ref: '#/$defs/PreparedContextBreakdown', }, { type: 'null', }, ], }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', + reportedInputTokens: { + description: + 'Provider-reported input tokens from this exact completed step, if available.', + type: ['integer', 'null'], format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - binding: { - $ref: '#/$defs/BindingStamp', + }, + additionalProperties: false, + required: [ + 'invocationId', + 'stepId', + 'sequence', + 'recordedAt', + 'sourceThrough', + 'modelId', + 'contextWindow', + 'inputLimit', + 'state', + 'breakdown', + 'reportedInputTokens', + ], + }, + AgentContextRequestState: { + type: 'string', + enum: ['requested', 'completed', 'interrupted'], + }, + PreparedContextBreakdown: { + description: + 'Host-prepared adapter input, not provider tokenization or HTTP wire bytes.', + type: 'object', + properties: { + system: { + $ref: '#/$defs/ContextBucket', }, - id: { - type: 'string', + user: { + $ref: '#/$defs/ContextBucket', }, - title: { - type: 'string', + assistant: { + $ref: '#/$defs/ContextBucket', }, - labels: { - type: 'array', - items: { - type: 'string', - }, + tool: { + description: + 'Messages with the tool role; assistant-embedded results stay in assistant.', + $ref: '#/$defs/ContextBucket', }, - workspace: { - $ref: '#/$defs/AgentWorkspace', + toolSchemas: { + $ref: '#/$defs/ContextBucket', }, - revision: { + counts: { + $ref: '#/$defs/ContextCounts', + }, + }, + additionalProperties: false, + required: ['system', 'user', 'assistant', 'tool', 'toolSchemas', 'counts'], + }, + ContextBucket: { + type: 'object', + properties: { + contentBytes: { + description: + 'UTF-8 text and compact JSON payload bytes counted by the Host heuristic.\nExcludes file encodings, message metadata, request-level provider options\nand synthetic framing overhead. Tool definitions are counted in full.', type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - updatedAt: { + estimatedTokens: { + description: + 'Heuristic: separately rounded text/JSON bytes / 3, plus message/tool\nframing and 1024 per media part. Never provider-measured tokens.', type: 'integer', format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - archived: { - type: 'boolean', + }, + additionalProperties: false, + required: ['contentBytes', 'estimatedTokens'], + }, + ContextCounts: { + type: 'object', + properties: { + messages: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - pinned: { - type: 'boolean', + media: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - hasUnread: { - type: 'boolean', + toolCalls: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], + toolResults: { + description: 'Result parts across both tool and assistant roles.', + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - activity: { - $ref: '#/$defs/AgentSessionActivity', + tools: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], - }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], + required: ['messages', 'media', 'toolCalls', 'toolResults', 'tools'], }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ - { - description: 'Capture an empty history prefix.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', - }, - }, - required: ['kind'], - additionalProperties: false, - }, + }, + }, + resources: {}, + }, + create: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCreate', + type: 'object', + properties: { + backend: { + description: 'Initial next-turn preference; admission resolves it again.', + anyOf: [ { - description: 'Include the complete target Turn.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'through_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + $ref: '#/$defs/AgentBackendPreference', }, { - description: - 'Exclude the complete target Turn, including its submitted inputs.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + type: 'null', }, ], }, + automaticTitle: { + description: + 'Treat title as a placeholder for one title derived from the first user message.', + type: 'boolean', + default: false, + }, + tools: { + description: + "Immutable narrowing of this Agent's installed tools. None uses the installation;\nan empty list permits no model tools. External executors cannot enforce this.", + type: ['array', 'null'], + items: { + type: 'string', + }, + default: null, + uniqueItems: true, + }, + title: { + description: + 'A concise, specific task name people can recognize in navigation; avoid generic worker numbers.', + type: 'string', + minLength: 1, + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspaceSelection', + }, + }, + additionalProperties: false, + required: ['title', 'labels', 'workspace'], + $defs: { AgentBackendPreference: { description: "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", @@ -3168,316 +3342,341 @@ export const contracts = { description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', + AgentWorkspaceSelection: { + oneOf: [ + { + description: 'A Git worktree of the directory at `path`, managed by the Host.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'isolated_git', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], + }, + { + description: 'A registered Project at one of its locations.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'project', + }, + project: { + $ref: '#/$defs/AgentProjectRef', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'project', 'path'], + }, + { + description: + 'The directory at `path`: absolute, `~` standing for the home directory,\nwithin the configured workspace locations.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'directory', + }, + path: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'path'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session', + }, + sessionId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId'], + }, + ], + }, + AgentProjectRef: { type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + id: { type: 'string', format: 'uuid', }, - namespace: { - $ref: '#/$defs/InstanceId', + revision: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + required: ['id', 'revision'], }, - AgentWorkspacePermissions: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentCreated', + oneOf: [ + { type: 'object', properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', + kind: { + type: 'string', + const: 'created', }, - requestPermissions: { - type: 'boolean', + session: { + $ref: '#/$defs/AgentSession', }, }, additionalProperties: false, - required: ['access', 'requestPermissions'], + required: ['kind', 'session'], }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], - }, - BindingStamp: { + { type: 'object', properties: { - id: { + kind: { type: 'string', - format: 'uuid', + const: 'rejected', }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, + reason: { + $ref: '#/$defs/AgentRejection', }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['kind', 'reason'], }, - AgentWorkspace: { + ], + $defs: { + AgentSession: { type: 'object', properties: { - worktree: { + parentSessionId: { description: - 'Native owner allocation; never accepted from a caller as workspace authority.', + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { anyOf: [ { - $ref: '#/$defs/Binding', + $ref: '#/$defs/AgentSessionBranch', }, { type: 'null', }, ], }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - path: { - type: 'string', - minLength: 1, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + dataSource: { + $ref: '#/$defs/AgentDataSource', }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', }, - }, - additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], - }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', - type: 'object', - properties: { id: { type: 'string', }, - common_dir: { + title: { type: 'string', }, - source: { - type: 'string', + labels: { + type: 'array', + items: { + type: 'string', + }, }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + workspace: { + $ref: '#/$defs/AgentWorkspace', }, - base_commit: { - type: 'string', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - directory: { - type: 'string', + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', }, }, additionalProperties: false, required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], - }, - AgentRejection: { - type: 'string', - enum: [ - 'permission_change_requires_user', - 'title_invalid', - 'labels_invalid', - 'tools_unavailable', - 'busy', - 'workspace_unavailable', - 'session_unavailable', - 'history_unavailable', - 'managed_session', - 'closed', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', ], }, - }, - }, - resources: {}, - }, - diagnostics: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDiagnostics', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentDiagnosticPosition', + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', + }, + }, + additionalProperties: false, + required: ['sessionId', 'position'], }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - $defs: { - AgentDiagnosticPosition: { + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', oneOf: [ { + description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'latest', + const: 'start', }, }, required: ['kind'], additionalProperties: false, }, { + description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'before', + const: 'end', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', }, - sequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + turnId: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'through', 'sequence'], + required: ['kind', 'turnId'], + }, + { + description: + 'Exclude the complete target Turn, including its submitted inputs.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, ], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDiagnosticPage', - type: 'object', - properties: { - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentDiagnostic', - }, - }, - nextBefore: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['through', 'entries'], - $defs: { - AgentDiagnostic: { - type: 'object', - properties: { - sequence: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - eventId: { - type: 'string', - }, - recordedAt: { - type: 'number', - format: 'double', - }, - detail: { - $ref: '#/$defs/DiagnosticDetail', - }, - }, - additionalProperties: false, - required: ['sequence', 'eventId', 'recordedAt', 'detail'], - }, - DiagnosticDetail: { + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'execution', - }, - invocation: { - $ref: '#/$defs/Invocation', - }, - reason: { - $ref: '#/$defs/ExecutionFailureReason', - }, - modelFailure: { - anyOf: [ - { - $ref: '#/$defs/ModelFailureCause', - }, - { - type: 'null', - }, - ], + const: 'model', }, - }, - additionalProperties: false, - required: ['kind', 'invocation', 'reason'], - }, - { - type: 'object', - properties: { - kind: { + connectionId: { type: 'string', - const: 'model_step', - }, - invocation: { - $ref: '#/$defs/Invocation', + minLength: 1, }, - stepId: { + modelId: { type: 'string', + minLength: 1, }, - status: { - $ref: '#/$defs/ModelInterruption', - }, - modelFailure: { + thinkingLevel: { anyOf: [ { - $ref: '#/$defs/ModelFailureCause', + $ref: '#/$defs/ThinkingLevel', }, { type: 'null', @@ -3486,630 +3685,560 @@ export const contracts = { }, }, additionalProperties: false, - required: ['kind', 'invocation', 'stepId', 'status'], + required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'session_title', - }, - invocation: { - $ref: '#/$defs/Invocation', + const: 'executor', }, - cause: { - $ref: '#/$defs/TitleFailureCause', + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, additionalProperties: false, - required: ['kind', 'invocation', 'cause'], + required: ['kind', 'selection'], }, ], }, - Invocation: { + ThinkingLevel: { + type: 'string', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], + }, + AgentExecutorSelection: { type: 'object', properties: { - session_id: { - type: 'string', - }, - turn_id: { - type: 'string', - }, - run_id: { + executor: { type: 'string', + minLength: 1, }, - invocation_id: { - type: 'string', + settings: { + $ref: '#/$defs/ExecutorSettings', }, }, - required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], - }, - ExecutionFailureReason: { - type: 'string', - enum: [ - 'session_busy', - 'continuation_changed', - 'reconciliation_required', - 'invalid_input', - 'cancelled', - 'model_incomplete', - 'event_commit', - 'event_store', - 'model_timeout', - 'model_http', - 'model_adapter', - 'model_provider', - 'context_overflow', - 'tool_execution', - 'runtime_internal', - 'cleanup_unconfirmed', - 'executor', - 'message_preparation_failed', - 'host_interrupted', - 'outcome_unknown', - 'execution_failed', - ], + additionalProperties: false, + required: ['executor', 'settings'], }, - ModelFailureCause: { - description: 'Safe failure evidence, separate from provider-owned diagnostic text.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'http', - }, - status: { - type: 'integer', - format: 'uint16', - minimum: 100, - maximum: 599, - }, - }, - additionalProperties: false, - required: ['kind', 'status'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'timed_out', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'context_overflow', - }, - }, - required: ['kind'], - additionalProperties: false, + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', + type: 'object', + properties: { + model: { + type: ['string', 'null'], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'provider', - }, - reason: { - $ref: '#/$defs/ProviderFailureReason', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', }, - retry_at_unix_ms: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'reason'], + ], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'adapter', - }, - }, - required: ['kind'], - additionalProperties: false, + runtime: { + type: ['string', 'null'], }, - ], + }, + additionalProperties: false, }, - ProviderFailureReason: { + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', - enum: [ - 'network', - 'timed_out', - 'rate_limit', - 'quota_exhausted', - 'provider_unavailable', - 'stream_truncated', - ], }, - ModelInterruption: { - oneOf: [ - { - type: 'string', - enum: ['steered', 'cancelled', 'timed_out', 'failed'], - }, - { - description: - 'Trusted ingress permits retry: completion-only steps can discard observations;\nitem-accepting steps must retain completed items and settle their tools.\nThis records safety evidence, not an instruction to retry.', + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { type: 'string', - const: 'retryable_failure', - }, - ], - }, - TitleFailureCause: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'preparation', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'model', - }, - cause: { - $ref: '#/$defs/ModelFailureCause', - }, - }, - additionalProperties: false, - required: ['kind', 'cause'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'invalid_output', - }, - }, - required: ['kind'], - additionalProperties: false, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'output_limit', - }, - }, - required: ['kind'], - additionalProperties: false, + deployment: { + type: 'string', + format: 'uuid', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'empty_output', - }, - }, - required: ['kind'], - additionalProperties: false, + namespace: { + $ref: '#/$defs/InstanceId', }, - ], + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'namespace'], }, - }, - }, - resources: {}, - }, - directories: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectories', - oneOf: [ - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { type: 'object', properties: { - kind: { - type: 'string', - const: 'roots', + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', }, }, - required: ['kind'], additionalProperties: false, + required: ['access', 'requestPermissions'], }, - { + AgentWorkspaceAccess: { description: - 'The directories inside `path`, a page at a time; `after` is the last\nname of the previous page.', + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { type: 'object', properties: { - kind: { - type: 'string', - const: 'children', - }, - path: { + id: { type: 'string', - minLength: 1, + format: 'uuid', }, - after: { - type: ['string', 'null'], + permissionEpoch: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'path', 'after'], + required: ['id', 'permissionEpoch'], }, - ], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectoryPage', - oneOf: [ - { + AgentWorkspace: { type: 'object', properties: { - kind: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], + }, + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { type: 'string', - const: 'roots', + minLength: 1, }, - roots: { - type: 'array', - items: { - $ref: '#/$defs/AgentDirectory', - }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: ['kind', 'roots'], + required: ['path', 'directoryIdentity', 'identity'], }, - { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { - kind: { + id: { type: 'string', - const: 'children', }, - directory: { - $ref: '#/$defs/AgentDirectory', + common_dir: { + type: 'string', }, - children: { - type: 'array', - items: { - $ref: '#/$defs/AgentDirectory', - }, - maxItems: 32, + source: { + type: 'string', }, - after: { - type: ['string', 'null'], + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - }, - additionalProperties: false, - required: ['kind', 'directory', 'children', 'after'], - }, - ], - $defs: { - AgentDirectory: { - type: 'object', - properties: { - name: { + base_commit: { type: 'string', }, - path: { + directory: { type: 'string', }, }, additionalProperties: false, - required: ['name', 'path'], + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], + }, + AgentSessionActivity: { + type: 'string', + enum: ['idle', 'working', 'waiting'], + }, + AgentRejection: { + type: 'string', + enum: [ + 'permission_change_requires_user', + 'title_invalid', + 'labels_invalid', + 'tools_unavailable', + 'busy', + 'workspace_unavailable', + 'session_unavailable', + 'history_unavailable', + 'managed_session', + 'closed', + ], }, }, }, resources: {}, }, - editQueue: { - mode: 'mutation', + diagnostics: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentQueueMutation', + title: 'AgentDiagnostics', type: 'object', properties: { sessionId: { type: 'string', minLength: 1, }, - expectedRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - edit: { - $ref: '#/$defs/AgentQueueAction', + position: { + $ref: '#/$defs/AgentDiagnosticPosition', }, }, additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'edit'], + required: ['sessionId', 'position'], $defs: { - AgentQueueAction: { + AgentDiagnosticPosition: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'retract', + const: 'latest', }, - messageId: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'before', + }, + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + sequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['kind', 'messageId'], + required: ['kind', 'through', 'sequence'], + }, + ], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentDiagnosticPage', + type: 'object', + properties: { + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentDiagnostic', + }, + }, + nextBefore: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['through', 'entries'], + $defs: { + AgentDiagnostic: { + type: 'object', + properties: { + sequence: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + eventId: { + type: 'string', + }, + recordedAt: { + type: 'number', + format: 'double', + }, + detail: { + $ref: '#/$defs/DiagnosticDetail', }, + }, + additionalProperties: false, + required: ['sequence', 'eventId', 'recordedAt', 'detail'], + }, + DiagnosticDetail: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'promote', + const: 'execution', }, - messageId: { - type: 'string', - minLength: 1, + invocation: { + $ref: '#/$defs/Invocation', + }, + reason: { + $ref: '#/$defs/ExecutionFailureReason', + }, + modelFailure: { + anyOf: [ + { + $ref: '#/$defs/ModelFailureCause', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['kind', 'messageId'], + required: ['kind', 'invocation', 'reason'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'update', + const: 'model_step', }, - messageId: { - type: 'string', - minLength: 1, + invocation: { + $ref: '#/$defs/Invocation', }, - message: { + stepId: { type: 'string', }, + status: { + $ref: '#/$defs/ModelInterruption', + }, + modelFailure: { + anyOf: [ + { + $ref: '#/$defs/ModelFailureCause', + }, + { + type: 'null', + }, + ], + }, }, additionalProperties: false, - required: ['kind', 'messageId', 'message'], + required: ['kind', 'invocation', 'stepId', 'status'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'reorder', + const: 'session_title', }, - messageIds: { - type: 'array', - items: { - type: 'string', - }, - minItems: 1, + invocation: { + $ref: '#/$defs/Invocation', + }, + cause: { + $ref: '#/$defs/TitleFailureCause', }, }, additionalProperties: false, - required: ['kind', 'messageIds'], + required: ['kind', 'invocation', 'cause'], }, ], }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentQueueMutationResult', - oneOf: [ - { + Invocation: { type: 'object', properties: { - kind: { + session_id: { type: 'string', - const: 'committed', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, }, - }, - additionalProperties: false, - required: ['kind', 'revision'], - }, - { - type: 'object', - properties: { - kind: { + turn_id: { type: 'string', - const: 'revision_conflict', - }, - expected: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - actual: { - type: 'integer', - format: 'uint64', - minimum: 0, }, - }, - additionalProperties: false, - required: ['kind', 'expected', 'actual'], - }, - { - type: 'object', - properties: { - kind: { + run_id: { type: 'string', - const: 'rejected', }, - reason: { - $ref: '#/$defs/AgentQueueRejection', + invocation_id: { + type: 'string', }, }, - additionalProperties: false, - required: ['kind', 'reason'], + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], }, - ], - $defs: { - AgentQueueRejection: { + ExecutionFailureReason: { type: 'string', enum: [ - 'not_your_message', - 'closed', - 'missing', - 'in_flight', - 'invalid_edit', - 'execution_changed', - 'not_running', + 'session_busy', + 'continuation_changed', + 'reconciliation_required', + 'invalid_input', + 'cancelled', + 'model_incomplete', + 'event_commit', + 'event_store', + 'model_timeout', + 'model_http', + 'model_adapter', + 'model_provider', + 'context_overflow', + 'tool_execution', + 'runtime_internal', + 'cleanup_unconfirmed', + 'executor', + 'message_preparation_failed', + 'host_interrupted', + 'outcome_unknown', + 'execution_failed', ], }, - }, - }, - resources: {}, - }, - execution: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExecutionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - invocationId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId', 'invocationId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExecutionResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'missing', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'found', - }, - execution: { - $ref: '#/$defs/AgentExecution', - }, - }, - additionalProperties: false, - required: ['kind', 'execution'], - }, - ], - $defs: { - AgentExecution: { - description: - 'Canonical physical execution receipt, independent of the current Session tip.', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - turnId: { - type: 'string', + ModelFailureCause: { + description: 'Safe failure evidence, separate from provider-owned diagnostic text.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'http', + }, + status: { + type: 'integer', + format: 'uint16', + minimum: 100, + maximum: 599, + }, + }, + additionalProperties: false, + required: ['kind', 'status'], }, - runId: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'timed_out', + }, + }, + required: ['kind'], + additionalProperties: false, }, - invocationId: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'context_overflow', + }, + }, + required: ['kind'], + additionalProperties: false, }, - highWater: { - description: 'Run-local event count, not the root-global transcript cursor.', - type: 'integer', - format: 'uint64', - minimum: 0, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'provider', + }, + reason: { + $ref: '#/$defs/ProviderFailureReason', + }, + retry_at_unix_ms: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'reason'], }, - state: { - $ref: '#/$defs/AgentExecutionState', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'adapter', + }, + }, + required: ['kind'], + additionalProperties: false, }, - }, - additionalProperties: false, - required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], + ], }, - AgentExecutionState: { + ProviderFailureReason: { type: 'string', enum: [ - 'admitted', - 'running', - 'waiting', - 'paused', - 'completed', - 'failed', - 'cancelled', + 'network', + 'timed_out', + 'rate_limit', + 'quota_exhausted', + 'provider_unavailable', + 'stream_truncated', ], }, - }, - }, - resources: {}, - }, - executorSetup: { - mode: 'mutation', - input: { - type: 'object', - additionalProperties: false, - properties: { - executor: { - type: 'string', - minLength: 1, - }, - action: { - $ref: '#/$defs/ExecutorSetupAction', + ModelInterruption: { + oneOf: [ + { + type: 'string', + enum: ['steered', 'cancelled', 'timed_out', 'failed'], + }, + { + description: + 'Trusted ingress permits retry: completion-only steps can discard observations;\nitem-accepting steps must retain completed items and settle their tools.\nThis records safety evidence, not an instruction to retry.', + type: 'string', + const: 'retryable_failure', + }, + ], }, - }, - required: ['executor', 'action'], - title: 'AgentExecutorSetup', - $schema: 'https://json-schema.org/draft/2020-12/schema', - $defs: { - ExecutorSetupAction: { + TitleFailureCause: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'check', + const: 'preparation', }, }, required: ['kind'], @@ -4117,459 +4246,733 @@ export const contracts = { }, { type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', + }, + cause: { + $ref: '#/$defs/ModelFailureCause', + }, + }, additionalProperties: false, + required: ['kind', 'cause'], + }, + { + type: 'object', properties: { kind: { type: 'string', - const: 'authenticate', + const: 'invalid_output', }, - methodId: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { type: 'string', - minLength: 1, + const: 'output_limit', }, }, - required: ['kind', 'methodId'], + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'empty_output', + }, + }, + required: ['kind'], + additionalProperties: false, }, ], }, }, }, - output: { + resources: {}, + }, + directories: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentDirectories', oneOf: [ - { - type: 'object', - additionalProperties: false, - properties: { - kind: { - type: 'string', - const: 'checked', - }, - info: { - anyOf: [ - { - $ref: '#/$defs/ExecutorSetupInfo', - }, - { - type: 'null', - }, - ], - }, - methods: { - type: 'array', - items: { - $ref: '#/$defs/ExecutorAuthenticationMethod', - }, - }, - }, - required: ['kind', 'methods'], - }, { type: 'object', properties: { kind: { type: 'string', - const: 'authenticated', + const: 'roots', }, }, required: ['kind'], additionalProperties: false, }, { + description: + 'The directories inside `path`, a page at a time; `after` is the last\nname of the previous page.', type: 'object', - additionalProperties: false, properties: { kind: { type: 'string', - const: 'unresolved', + const: 'children', }, - diagnostic: { + path: { type: 'string', + minLength: 1, + }, + after: { + type: ['string', 'null'], }, }, - required: ['kind', 'diagnostic'], - description: - 'No authentication confirmation was observed; never automatically replay it.', + additionalProperties: false, + required: ['kind', 'path', 'after'], }, ], - title: 'ExecutorSetupResult', + }, + output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - $defs: { - ExecutorSetupInfo: { + title: 'AgentDirectoryPage', + oneOf: [ + { type: 'object', - additionalProperties: false, properties: { - title: { - type: ['string', 'null'], - }, - name: { + kind: { type: 'string', + const: 'roots', }, - version: { - type: 'string', + roots: { + type: 'array', + items: { + $ref: '#/$defs/AgentDirectory', + }, }, }, - required: ['name', 'version'], + additionalProperties: false, + required: ['kind', 'roots'], }, - ExecutorAuthenticationMethod: { + { type: 'object', - additionalProperties: false, properties: { - id: { + kind: { type: 'string', - minLength: 1, + const: 'children', }, - name: { - type: 'string', - minLength: 1, + directory: { + $ref: '#/$defs/AgentDirectory', }, - description: { - type: ['string', 'null'], + children: { + type: 'array', + items: { + $ref: '#/$defs/AgentDirectory', + }, + maxItems: 32, }, - supported: { - type: 'boolean', + after: { + type: ['string', 'null'], }, }, - required: ['id', 'name', 'supported'], + additionalProperties: false, + required: ['kind', 'directory', 'children', 'after'], }, - }, - }, - progress: { - oneOf: [ - { + ], + $defs: { + AgentDirectory: { type: 'object', - additionalProperties: false, properties: { - kind: { + name: { type: 'string', - const: 'authorization_url', }, - url: { + path: { type: 'string', }, }, - required: ['kind', 'url'], - }, - ], - title: 'ExecutorSetupProgress', - $schema: 'https://json-schema.org/draft/2020-12/schema', - }, - resources: {}, - }, - executors: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExecutorsRead', - type: 'object', - additionalProperties: false, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExecutors', - type: 'object', - properties: { - executors: { - type: 'array', - items: { - type: 'string', - }, - }, - setup: { - type: 'array', - items: { - type: 'string', - }, + additionalProperties: false, + required: ['name', 'path'], }, }, - additionalProperties: false, - required: ['executors', 'setup'], }, resources: {}, }, - externalAgents: { - mode: 'query', + editQueue: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExternalAgentsRead', - type: 'object', - properties: { - inspect: { - type: 'boolean', - default: false, - }, - }, - additionalProperties: false, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentExternalAgents', + title: 'AgentQueueMutation', type: 'object', properties: { - target: { - $ref: '#/$defs/AgentExternalAgentsTarget', - }, - configured: { - type: 'boolean', + sessionId: { + type: 'string', + minLength: 1, }, - runtimes: { - type: 'array', - items: { - $ref: '#/$defs/AgentExternalRuntimeOption', - }, + expectedRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - state: { - $ref: '#/$defs/AgentExternalAgentsState', + edit: { + $ref: '#/$defs/AgentQueueAction', }, }, additionalProperties: false, - required: ['target', 'configured', 'runtimes', 'state'], + required: ['sessionId', 'expectedRevision', 'edit'], $defs: { - AgentExternalAgentsTarget: { - type: 'object', - properties: { - source: { - $ref: '#/$defs/AgentDataSource', - }, - path: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - additionalProperties: false, - required: ['source', 'path'], - }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', - type: 'object', - properties: { - stateDomain: { - type: 'string', - }, - deployment: { - type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, - }, - AgentExternalRuntimeOption: { - type: 'object', - properties: { - id: { - type: 'string', - }, - name: { - type: 'string', - }, - enabled: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['id', 'name', 'enabled'], - }, - AgentExternalAgentsState: { + AgentQueueAction: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'disabled', + const: 'retract', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { + messageId: { type: 'string', - const: 'enabled', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'messageId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'workspace_required', + const: 'promote', + }, + messageId: { + type: 'string', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'messageId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'unavailable', + const: 'update', + }, + messageId: { + type: 'string', + minLength: 1, }, message: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'message'], + required: ['kind', 'messageId', 'message'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'ready', + const: 'reorder', }, - runtimes: { + messageIds: { type: 'array', items: { - $ref: '#/$defs/AgentExternalRuntime', + type: 'string', }, + minItems: 1, }, }, additionalProperties: false, - required: ['kind', 'runtimes'], + required: ['kind', 'messageIds'], }, ], }, - AgentExternalRuntime: { + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentQueueMutationResult', + oneOf: [ + { type: 'object', properties: { - id: { + kind: { type: 'string', + const: 'committed', }, - name: { + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'revision'], + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'revision_conflict', }, - installation: { - $ref: '#/$defs/AgentExternalInstallation', + expected: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - authentication: { - $ref: '#/$defs/AgentExternalAuthentication', + actual: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - models: { - $ref: '#/$defs/AgentExternalModels', + }, + additionalProperties: false, + required: ['kind', 'expected', 'actual'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', + }, + reason: { + $ref: '#/$defs/AgentQueueRejection', }, }, additionalProperties: false, - required: ['id', 'name', 'installation', 'authentication', 'models'], + required: ['kind', 'reason'], }, - AgentExternalInstallation: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'available', - }, - version: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - required: ['kind'], + ], + $defs: { + AgentQueueRejection: { + type: 'string', + enum: [ + 'not_your_message', + 'closed', + 'missing', + 'in_flight', + 'invalid_edit', + 'execution_changed', + 'not_running', + ], + }, + }, + }, + resources: {}, + }, + execution: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExecutionRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + invocationId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId', 'invocationId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExecutionResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'missing', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'not_found', - }, - }, - required: ['kind'], - additionalProperties: false, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'found', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unsupported', - }, - detail: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'detail'], + execution: { + $ref: '#/$defs/AgentExecution', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'error', - }, - detail: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'detail'], + }, + additionalProperties: false, + required: ['kind', 'execution'], + }, + ], + $defs: { + AgentExecution: { + description: + 'Canonical physical execution receipt, independent of the current Session tip.', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + turnId: { + type: 'string', + }, + runId: { + type: 'string', + }, + invocationId: { + type: 'string', + }, + highWater: { + description: 'Run-local event count, not the root-global transcript cursor.', + type: 'integer', + format: 'uint64', + minimum: 0, }, + state: { + $ref: '#/$defs/AgentExecutionState', + }, + }, + additionalProperties: false, + required: ['sessionId', 'turnId', 'runId', 'invocationId', 'highWater', 'state'], + }, + AgentExecutionState: { + type: 'string', + enum: [ + 'admitted', + 'running', + 'waiting', + 'paused', + 'completed', + 'failed', + 'cancelled', ], }, - AgentExternalAuthentication: { + }, + }, + resources: {}, + }, + executorSetup: { + mode: 'mutation', + input: { + type: 'object', + additionalProperties: false, + properties: { + executor: { + type: 'string', + minLength: 1, + }, + action: { + $ref: '#/$defs/ExecutorSetupAction', + }, + }, + required: ['executor', 'action'], + title: 'AgentExecutorSetup', + $schema: 'https://json-schema.org/draft/2020-12/schema', + $defs: { + ExecutorSetupAction: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'authenticated', - }, - source: { - type: 'string', + const: 'check', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'source'], }, { type: 'object', + additionalProperties: false, properties: { kind: { type: 'string', - const: 'unauthenticated', + const: 'authenticate', }, - source: { + methodId: { type: 'string', - }, - detail: { - type: ['string', 'null'], + minLength: 1, }, }, - additionalProperties: false, - required: ['kind', 'source'], + required: ['kind', 'methodId'], }, - { - type: 'object', - properties: { - kind: { + ], + }, + }, + }, + output: { + oneOf: [ + { + type: 'object', + additionalProperties: false, + properties: { + kind: { + type: 'string', + const: 'checked', + }, + info: { + anyOf: [ + { + $ref: '#/$defs/ExecutorSetupInfo', + }, + { + type: 'null', + }, + ], + }, + methods: { + type: 'array', + items: { + $ref: '#/$defs/ExecutorAuthenticationMethod', + }, + }, + }, + required: ['kind', 'methods'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'authenticated', + }, + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + additionalProperties: false, + properties: { + kind: { + type: 'string', + const: 'unresolved', + }, + diagnostic: { + type: 'string', + }, + }, + required: ['kind', 'diagnostic'], + description: + 'No authentication confirmation was observed; never automatically replay it.', + }, + ], + title: 'ExecutorSetupResult', + $schema: 'https://json-schema.org/draft/2020-12/schema', + $defs: { + ExecutorSetupInfo: { + type: 'object', + additionalProperties: false, + properties: { + title: { + type: ['string', 'null'], + }, + name: { + type: 'string', + }, + version: { + type: 'string', + }, + }, + required: ['name', 'version'], + }, + ExecutorAuthenticationMethod: { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + minLength: 1, + }, + name: { + type: 'string', + minLength: 1, + }, + description: { + type: ['string', 'null'], + }, + supported: { + type: 'boolean', + }, + }, + required: ['id', 'name', 'supported'], + }, + }, + }, + progress: { + oneOf: [ + { + type: 'object', + additionalProperties: false, + properties: { + kind: { + type: 'string', + const: 'authorization_url', + }, + url: { + type: 'string', + }, + }, + required: ['kind', 'url'], + }, + ], + title: 'ExecutorSetupProgress', + $schema: 'https://json-schema.org/draft/2020-12/schema', + }, + resources: {}, + }, + executors: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExecutorsRead', + type: 'object', + additionalProperties: false, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExecutors', + type: 'object', + properties: { + executors: { + type: 'array', + items: { + type: 'string', + }, + }, + setup: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + required: ['executors', 'setup'], + }, + resources: {}, + }, + externalAgents: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExternalAgentsRead', + type: 'object', + properties: { + inspect: { + type: 'boolean', + default: false, + }, + }, + additionalProperties: false, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentExternalAgents', + type: 'object', + properties: { + target: { + $ref: '#/$defs/AgentExternalAgentsTarget', + }, + configured: { + type: 'boolean', + }, + runtimes: { + type: 'array', + items: { + $ref: '#/$defs/AgentExternalRuntimeOption', + }, + }, + state: { + $ref: '#/$defs/AgentExternalAgentsState', + }, + }, + additionalProperties: false, + required: ['target', 'configured', 'runtimes', 'state'], + $defs: { + AgentExternalAgentsTarget: { + type: 'object', + properties: { + source: { + $ref: '#/$defs/AgentDataSource', + }, + path: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + required: ['source', 'path'], + }, + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + }, + additionalProperties: false, + required: ['stateDomain', 'deployment', 'namespace'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentExternalRuntimeOption: { + type: 'object', + properties: { + id: { + type: 'string', + }, + name: { + type: 'string', + }, + enabled: { + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['id', 'name', 'enabled'], + }, + AgentExternalAgentsState: { + oneOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', - const: 'unsupported', + const: 'disabled', }, }, required: ['kind'], @@ -4580,42 +4983,37 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'unknown', - }, - detail: { - type: ['string', 'null'], - }, - source: { - type: ['string', 'null'], - }, - configured: { - type: ['array', 'null'], - items: { - type: 'string', - }, + const: 'enabled', }, }, + required: ['kind'], additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workspace_required', + }, + }, required: ['kind'], + additionalProperties: false, }, { type: 'object', properties: { kind: { type: 'string', - const: 'error', + const: 'unavailable', }, - detail: { + message: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'detail'], + required: ['kind', 'message'], }, - ], - }, - AgentExternalModels: { - oneOf: [ { type: 'object', properties: { @@ -4623,23 +5021,50 @@ export const contracts = { type: 'string', const: 'ready', }, - count: { - type: 'integer', - format: 'uint', - minimum: 0, + runtimes: { + type: 'array', + items: { + $ref: '#/$defs/AgentExternalRuntime', + }, }, }, additionalProperties: false, - required: ['kind', 'count'], + required: ['kind', 'runtimes'], + }, + ], + }, + AgentExternalRuntime: { + type: 'object', + properties: { + id: { + type: 'string', }, + name: { + type: 'string', + }, + installation: { + $ref: '#/$defs/AgentExternalInstallation', + }, + authentication: { + $ref: '#/$defs/AgentExternalAuthentication', + }, + models: { + $ref: '#/$defs/AgentExternalModels', + }, + }, + additionalProperties: false, + required: ['id', 'name', 'installation', 'authentication', 'models'], + }, + AgentExternalInstallation: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'unauthenticated', + const: 'available', }, - detail: { + version: { type: ['string', 'null'], }, }, @@ -4651,25 +5076,25 @@ export const contracts = { properties: { kind: { type: 'string', - const: 'unsupported', - }, - detail: { - type: 'string', + const: 'not_found', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'detail'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'unavailable', + const: 'unsupported', + }, + detail: { + type: 'string', }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'detail'], }, { type: 'object', @@ -4687,844 +5112,420 @@ export const contracts = { }, ], }, - }, - }, - resources: {}, - }, - fileCandidates: { - mode: 'subscription', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentFileCandidatesRead', - description: - "Workspace-relative file suggestions from this Session's admitted directory.", - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - directory: { - description: 'Omission or null selects the workspace root.', - anyOf: [ - { - $ref: '#/$defs/WorkspacePath', - }, + AgentExternalAuthentication: { + oneOf: [ { - type: 'null', - }, - ], - }, - query: { - description: - "Empty or whitespace browses the selected directory's direct files and directories.\nOtherwise, searches file paths within that directory's descendants.", - type: 'string', - }, - }, - additionalProperties: false, - required: ['sessionId', 'query'], - $defs: { - WorkspacePath: { - type: 'object', - properties: { - bytesBase64: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['bytesBase64'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentFileCandidates', - anyOf: [ - { - $ref: '#/$defs/AgentFileCandidates', - }, - { - type: 'null', - }, - ], - $defs: { - AgentFileCandidates: { - description: - 'An emitted batch of path observations; null final completion ends the subscription.', - type: 'object', - properties: { - entries: { - type: 'array', - items: { - $ref: '#/$defs/AgentFileEntry', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'authenticated', + }, + source: { + type: 'string', + }, }, + additionalProperties: false, + required: ['kind', 'source'], }, - }, - additionalProperties: false, - required: ['entries'], - }, - AgentFileEntry: { - type: 'object', - properties: { - path: { - $ref: '#/$defs/WorkspacePath', - }, - kind: { - $ref: '#/$defs/AgentFileKind', - }, - }, - additionalProperties: false, - required: ['path', 'kind'], - }, - WorkspacePath: { - type: 'object', - properties: { - bytesBase64: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['bytesBase64'], - }, - AgentFileKind: { - type: 'string', - enum: ['file', 'directory', 'symlink', 'other'], - }, - }, - }, - resources: {}, - }, - fork: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentFork', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - expectedRevision: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - title: { - type: 'string', - minLength: 1, - }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'position', 'title', 'labels'], - $defs: { - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ { - description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'start', + const: 'unauthenticated', + }, + source: { + type: 'string', + }, + detail: { + type: ['string', 'null'], }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'source'], }, { - description: 'Capture all current history, including later compaction facts.', type: 'object', properties: { kind: { type: 'string', - const: 'end', + const: 'unsupported', }, }, required: ['kind'], additionalProperties: false, }, { - description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'through_turn', + const: 'unknown', }, - turnId: { - type: 'string', - minLength: 1, + detail: { + type: ['string', 'null'], + }, + source: { + type: ['string', 'null'], + }, + configured: { + type: ['array', 'null'], + items: { + type: 'string', + }, }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind'], }, { - description: - 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'before_turn', + const: 'error', }, - turnId: { + detail: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'turnId'], - }, - ], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentForkResult', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'forked', - }, - session: { - $ref: '#/$defs/AgentSession', - }, - }, - additionalProperties: false, - required: ['kind', 'session'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'revision_conflict', - }, - expected: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - actual: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['kind', 'expected', 'actual'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'rejected', - }, - reason: { - $ref: '#/$defs/AgentRejection', - }, - }, - additionalProperties: false, - required: ['kind', 'reason'], - }, - ], - $defs: { - AgentSession: { - type: 'object', - properties: { - parentSessionId: { - description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { - anyOf: [ - { - $ref: '#/$defs/AgentBackendPreference', - }, - { - type: 'null', - }, - ], - }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { - type: 'string', - }, - title: { - type: 'string', - }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - archived: { - type: 'boolean', - }, - pinned: { - type: 'boolean', - }, - hasUnread: { - type: 'boolean', - }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], - }, - activity: { - $ref: '#/$defs/AgentSessionActivity', + required: ['kind', 'detail'], }, - }, - additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', ], }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - position: { - $ref: '#/$defs/AgentForkPosition', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', + AgentExternalModels: { oneOf: [ { - description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'start', + const: 'ready', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', + count: { + type: 'integer', + format: 'uint', + minimum: 0, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'count'], }, { - description: 'Include the complete target Turn.', type: 'object', properties: { - kind: { - type: 'string', - const: 'through_turn', - }, - turnId: { + kind: { type: 'string', - minLength: 1, + const: 'unauthenticated', + }, + detail: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind'], }, { - description: - 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'before_turn', + const: 'unsupported', }, - turnId: { + detail: { type: 'string', - minLength: 1, }, }, additionalProperties: false, - required: ['kind', 'turnId'], + required: ['kind', 'detail'], }, - ], - }, - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], + const: 'unavailable', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'executor', + const: 'error', }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', + detail: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'selection'], + required: ['kind', 'detail'], }, ], }, - ThinkingLevel: { + }, + }, + resources: {}, + }, + fileCandidates: { + mode: 'subscription', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentFileCandidatesRead', + description: + "Workspace-relative file suggestions from this Session's admitted directory.", + type: 'object', + properties: { + sessionId: { type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - AgentExecutorSelection: { - type: 'object', - properties: { - executor: { - type: 'string', - minLength: 1, + directory: { + description: 'Omission or null selects the workspace root.', + anyOf: [ + { + $ref: '#/$defs/WorkspacePath', }, - settings: { - $ref: '#/$defs/ExecutorSettings', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['executor', 'settings'], + ], }, - ExecutorSettings: { + query: { description: - 'Executor-owned model selection, independent of Host model connections.', - type: 'object', - properties: { - model: { - type: ['string', 'null'], - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], - }, - runtime: { - type: ['string', 'null'], - }, - }, - additionalProperties: false, - }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", + "Empty or whitespace browses the selected directory's direct files and directories.\nOtherwise, searches file paths within that directory's descendants.", type: 'string', }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', + }, + additionalProperties: false, + required: ['sessionId', 'query'], + $defs: { + WorkspacePath: { type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + bytesBase64: { type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], + required: ['bytesBase64'], }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentFileCandidates', + anyOf: [ + { + $ref: '#/$defs/AgentFileCandidates', }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', - }, - requestPermissions: { - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['access', 'requestPermissions'], + { + type: 'null', }, - AgentWorkspaceAccess: { + ], + $defs: { + AgentFileCandidates: { description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], - }, - BindingStamp: { + 'An emitted batch of path observations; null final completion ends the subscription.', type: 'object', properties: { - id: { - type: 'string', - format: 'uuid', - }, - permissionEpoch: { - type: 'integer', - format: 'uint64', - minimum: 0, + entries: { + type: 'array', + items: { + $ref: '#/$defs/AgentFileEntry', + }, }, }, additionalProperties: false, - required: ['id', 'permissionEpoch'], + required: ['entries'], }, - AgentWorkspace: { + AgentFileEntry: { type: 'object', properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + $ref: '#/$defs/WorkspacePath', }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + kind: { + $ref: '#/$defs/AgentFileKind', }, }, additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], + required: ['path', 'kind'], }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', + WorkspacePath: { type: 'object', properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - base_commit: { - type: 'string', - }, - directory: { + bytesBase64: { type: 'string', }, }, additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], + required: ['bytesBase64'], }, - AgentRejection: { + AgentFileKind: { type: 'string', - enum: [ - 'permission_change_requires_user', - 'title_invalid', - 'labels_invalid', - 'tools_unavailable', - 'busy', - 'workspace_unavailable', - 'session_unavailable', - 'history_unavailable', - 'managed_session', - 'closed', - ], + enum: ['file', 'directory', 'symlink', 'other'], }, }, }, resources: {}, }, - forkReceipt: { - mode: 'query', + fork: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentForkReceipt', + title: 'AgentFork', type: 'object', properties: { - reference: { - description: - 'The complete original fork operation reference, including its fingerprint.', - $ref: '#/$defs/RecoveryRef', + sessionId: { + type: 'string', + minLength: 1, + }, + expectedRevision: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + position: { + $ref: '#/$defs/AgentForkPosition', + }, + title: { + type: 'string', + minLength: 1, + }, + labels: { + type: 'array', + items: { + type: 'string', + }, }, }, additionalProperties: false, - required: ['reference'], + required: ['sessionId', 'expectedRevision', 'position', 'title', 'labels'], $defs: { - RecoveryRef: { - type: 'object', - properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', + oneOf: [ + { + description: 'Capture an empty history prefix.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, }, - export: { - type: 'string', + { + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + }, + required: ['kind'], + additionalProperties: false, }, - method: { - type: 'string', + { + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, - operation: { - type: 'string', - format: 'uuid', + { + description: + 'Exclude the complete target Turn, including its submitted inputs.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, - fingerprint: { + ], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentForkResult', + oneOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'forked', }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + session: { + $ref: '#/$defs/AgentSession', }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['kind', 'session'], }, - DeploymentRef: { + { type: 'object', properties: { - stateDomain: { + kind: { type: 'string', + const: 'revision_conflict', }, - deployment: { - type: 'string', - format: 'uuid', + expected: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - revision: { + actual: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + required: ['kind', 'expected', 'actual'], }, - RecoveryContract: { - description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + { type: 'object', properties: { - name: { + kind: { type: 'string', + const: 'rejected', }, - version: { - type: 'integer', - format: 'uint32', - minimum: 0, - }, - transport: { - $ref: '#/$defs/Transport', - }, - digest: { - type: 'string', + reason: { + $ref: '#/$defs/AgentRejection', }, }, additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], - }, - Transport: { - type: 'string', - enum: ['local', 'remote'], - }, - RecoveryProtocol: { - type: 'string', - enum: ['journal_v1'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentForkReceiptResult', - type: 'object', - properties: { - session: { - description: - 'A currently readable published fork. Null neither proves no effect nor\nauthorizes replacing or redispatching the original operation.', - anyOf: [ - { - $ref: '#/$defs/AgentSession', - }, - { - type: 'null', - }, - ], + required: ['kind', 'reason'], }, - }, - additionalProperties: false, - required: ['session'], + ], $defs: { AgentSession: { type: 'object', @@ -5948,717 +5949,804 @@ export const contracts = { type: 'string', enum: ['idle', 'working', 'waiting'], }, + AgentRejection: { + type: 'string', + enum: [ + 'permission_change_requires_user', + 'title_invalid', + 'labels_invalid', + 'tools_unavailable', + 'busy', + 'workspace_unavailable', + 'session_unavailable', + 'history_unavailable', + 'managed_session', + 'closed', + ], + }, }, }, resources: {}, }, - generateRecap: { - mode: 'mutation', + forkReceipt: { + mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentRecapGenerate', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - model: { - $ref: '#/$defs/ModelSelection', + title: 'AgentForkReceipt', + type: 'object', + properties: { + reference: { + description: + 'The complete original fork operation reference, including its fingerprint.', + $ref: '#/$defs/RecoveryRef', }, }, additionalProperties: false, - required: ['sessionId', 'model'], + required: ['reference'], $defs: { - ModelSelection: { - description: - 'Stable model identity, resolved from current connection settings at admission.', + RecoveryRef: { type: 'object', properties: { - connectionId: { - type: 'string', - minLength: 1, + source: { + $ref: '#/$defs/DeploymentRef', }, - modelId: { + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { type: 'string', - minLength: 1, }, - }, - required: ['connectionId', 'modelId'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentRecapReceipt', - oneOf: [ - { - type: 'object', - properties: { - kind: { + method: { type: 'string', - const: 'pending', }, - operationId: { + operation: { type: 'string', format: 'uuid', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', }, }, additionalProperties: false, - required: ['kind', 'operationId', 'through'], + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], }, - { + DeploymentRef: { type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'ready', }, - operationId: { + deployment: { type: 'string', format: 'uuid', }, - through: { + revision: { type: 'integer', format: 'uint64', minimum: 0, }, - text: { - type: 'string', - }, - modelId: { - type: 'string', - }, }, additionalProperties: false, - required: ['kind', 'operationId', 'through', 'text', 'modelId'], + required: ['stateDomain', 'deployment', 'revision'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', type: 'object', properties: { - kind: { - type: 'string', - const: 'failed', - }, - operationId: { + name: { type: 'string', - format: 'uuid', }, - through: { + version: { type: 'integer', - format: 'uint64', + format: 'uint32', minimum: 0, }, - reason: { - $ref: '#/$defs/AgentRecapFailure', + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', }, }, additionalProperties: false, - required: ['kind', 'operationId', 'through', 'reason'], + required: ['name', 'version', 'transport', 'digest'], }, - ], - $defs: { - AgentRecapFailure: { + Transport: { type: 'string', - enum: ['empty_or_oversized_output', 'incomplete_output', 'model_unavailable'], + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], }, }, }, - resources: {}, - }, - gitDiff: { - mode: 'query', - input: { + output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGitDiffRead', + title: 'AgentForkReceiptResult', type: 'object', properties: { - sessionId: { - type: 'string', - }, - area: { - $ref: '#/$defs/GitArea', - }, - path: { - $ref: '#/$defs/WorkspacePath', + session: { + description: + 'A currently readable published fork. Null neither proves no effect nor\nauthorizes replacing or redispatching the original operation.', + anyOf: [ + { + $ref: '#/$defs/AgentSession', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['sessionId', 'area', 'path'], + required: ['session'], $defs: { - GitArea: { - type: 'string', - enum: ['staged', 'worktree', 'untracked'], + AgentSession: { + type: 'object', + properties: { + parentSessionId: { + description: + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { + anyOf: [ + { + $ref: '#/$defs/AgentSessionBranch', + }, + { + type: 'null', + }, + ], + }, + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], + }, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, + }, + dataSource: { + $ref: '#/$defs/AgentDataSource', + }, + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', + }, + id: { + type: 'string', + }, + title: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'string', + }, + }, + workspace: { + $ref: '#/$defs/AgentWorkspace', + }, + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', + }, + }, + additionalProperties: false, + required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', + 'id', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', + ], }, - WorkspacePath: { + AgentSessionBranch: { + description: + 'Frozen conversation ancestry for presentation, not authority over the source.', type: 'object', properties: { - bytesBase64: { + sessionId: { type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', }, }, additionalProperties: false, - required: ['bytesBase64'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_GitDiff', - anyOf: [ - { - $ref: '#/$defs/GitDiff', - }, - { - type: 'null', + required: ['sessionId', 'position'], }, - ], - $defs: { - GitDiff: { + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', oneOf: [ { + description: 'Capture an empty history prefix.', type: 'object', properties: { kind: { type: 'string', - const: 'text', + const: 'start', }, - patch: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { type: 'string', + const: 'end', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'patch'], }, { + description: 'Include the complete target Turn.', type: 'object', properties: { kind: { type: 'string', - const: 'binary', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'turnId'], }, { + description: + 'Exclude the complete target Turn, including its submitted inputs.', type: 'object', properties: { kind: { type: 'string', - const: 'unchanged', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'turnId'], }, + ], + }, + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'unsupported', + const: 'model', + }, + connectionId: { + type: 'string', + minLength: 1, + }, + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', + }, + selection: { + $ref: '#/$defs/AgentExecutorSelection', }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'selection'], }, ], }, - }, - }, - resources: {}, - }, - gitStatus: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { + ThinkingLevel: { type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_GitStatus', - anyOf: [ - { - $ref: '#/$defs/GitStatus', - }, - { - type: 'null', + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - ], - $defs: { - GitStatus: { + AgentExecutorSelection: { type: 'object', properties: { - head: { - type: ['string', 'null'], + executor: { + type: 'string', + minLength: 1, }, - entries: { - type: 'array', - items: { - $ref: '#/$defs/GitStatusEntry', - }, + settings: { + $ref: '#/$defs/ExecutorSettings', }, }, additionalProperties: false, - required: ['head', 'entries'], + required: ['executor', 'settings'], }, - GitStatusEntry: { + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - area: { - $ref: '#/$defs/GitArea', - }, - path: { - $ref: '#/$defs/WorkspacePath', + model: { + type: ['string', 'null'], }, - kind: { - $ref: '#/$defs/GitChangeKind', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], }, - }, - additionalProperties: false, - required: ['area', 'path', 'kind'], - }, - GitArea: { - type: 'string', - enum: ['staged', 'worktree', 'untracked'], - }, - WorkspacePath: { - type: 'object', - properties: { - bytesBase64: { - type: 'string', + runtime: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['bytesBase64'], }, - GitChangeKind: { + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', - enum: ['added', 'modified', 'deleted', 'type_changed', 'conflicted'], }, - }, - }, - resources: {}, - }, - globalInstructions: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGlobalInstructionsRead', - type: 'object', - additionalProperties: false, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGlobalInstructionsView', - oneOf: [ - { + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', type: 'object', properties: { - kind: { - type: 'string', - const: 'available', - }, - source: { - $ref: '#/$defs/AgentGlobalInstructionSource', - }, - path: { - type: 'string', - }, - revision: { + stateDomain: { type: 'string', }, - content: { + deployment: { type: 'string', + format: 'uuid', }, - exists: { - type: 'boolean', + namespace: { + $ref: '#/$defs/InstanceId', }, }, additionalProperties: false, - required: ['kind', 'source', 'path', 'revision', 'content', 'exists'], + required: ['stateDomain', 'deployment', 'namespace'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, }, - { + AgentWorkspacePermissions: { type: 'object', properties: { - kind: { - type: 'string', - const: 'unavailable', + access: { + $ref: '#/$defs/AgentWorkspaceAccess', + }, + requestPermissions: { + type: 'boolean', }, }, - required: ['kind'], additionalProperties: false, + required: ['access', 'requestPermissions'], }, - ], - $defs: { - AgentGlobalInstructionSource: { - type: 'string', - enum: ['maka', 'agents'], - }, - }, - }, - resources: {}, - }, - goal: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGoalRead', - type: 'object', - properties: { - sessionId: { + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGoalView', - type: 'object', - properties: { - enabled: { - type: 'boolean', - }, - current: { - anyOf: [ - { - $ref: '#/$defs/AgentGoalDetail', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['enabled'], - $defs: { - AgentGoalDetail: { + BindingStamp: { type: 'object', properties: { id: { type: 'string', format: 'uuid', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - objective: { - type: 'string', - }, - status: { - $ref: '#/$defs/GoalStatus', - }, - iterations: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - tokenBudget: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - consumed: { - $ref: '#/$defs/GoalMeter', - }, - remainingTokens: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - elapsedSeconds: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - retryAt: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - }, - createdAt: { + permissionEpoch: { type: 'integer', format: 'uint64', minimum: 0, }, - pending: { - type: 'boolean', - }, - report: { + }, + additionalProperties: false, + required: ['id', 'permissionEpoch'], + }, + AgentWorkspace: { + type: 'object', + properties: { + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', anyOf: [ { - $ref: '#/$defs/GoalReport', + $ref: '#/$defs/Binding', }, { type: 'null', }, ], }, - note: { + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', + }, + path: { + type: 'string', + minLength: 1, + }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + identity: { type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: [ - 'id', - 'revision', - 'objective', - 'status', - 'iterations', - 'consumed', - 'elapsedSeconds', - 'createdAt', - 'pending', - 'note', - ], - }, - GoalStatus: { - type: 'string', - enum: [ - 'armed', - 'active', - 'paused', - 'achieved', - 'cancelled', - 'budget_limited', - 'blocked', - ], + required: ['path', 'directoryIdentity', 'identity'], }, - GoalMeter: { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { - known: { - type: 'integer', - format: 'uint64', - minimum: 0, + id: { + type: 'string', }, - missing: { - type: 'integer', - format: 'uint64', - minimum: 0, + common_dir: { + type: 'string', }, - }, - required: ['known', 'missing'], - }, - GoalReport: { - type: 'object', - properties: { - status: { - $ref: '#/$defs/GoalReportKind', + source: { + type: 'string', }, - note: { + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { type: 'string', }, }, additionalProperties: false, - required: ['status', 'note'], + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], }, - GoalReportKind: { + AgentSessionActivity: { type: 'string', - enum: ['progress', 'achieved', 'blocked', 'paused'], + enum: ['idle', 'working', 'waiting'], }, }, }, resources: {}, }, - goalReceipt: { - mode: 'query', + generateRecap: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentGoalReceiptRead', + title: 'AgentRecapGenerate', type: 'object', properties: { - reference: { - $ref: '#/$defs/RecoveryRef', + sessionId: { + type: 'string', + }, + model: { + $ref: '#/$defs/ModelSelection', }, }, additionalProperties: false, - required: ['reference'], + required: ['sessionId', 'model'], $defs: { - RecoveryRef: { + ModelSelection: { + description: + 'Stable model identity, resolved from current connection settings at admission.', type: 'object', properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - export: { - type: 'string', - }, - method: { + connectionId: { type: 'string', + minLength: 1, }, - operation: { + modelId: { type: 'string', - format: 'uuid', + minLength: 1, }, - fingerprint: { + }, + required: ['connectionId', 'modelId'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentRecapReceipt', + oneOf: [ + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'pending', }, - contract: { - $ref: '#/$defs/RecoveryContract', + operationId: { + type: 'string', + format: 'uuid', }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + through: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['kind', 'operationId', 'through'], }, - DeploymentRef: { + { type: 'object', properties: { - stateDomain: { + kind: { type: 'string', + const: 'ready', }, - deployment: { + operationId: { type: 'string', format: 'uuid', }, - revision: { + through: { type: 'integer', format: 'uint64', minimum: 0, }, + text: { + type: 'string', + }, + modelId: { + type: 'string', + }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + required: ['kind', 'operationId', 'through', 'text', 'modelId'], }, - RecoveryContract: { - description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + { type: 'object', properties: { - name: { + kind: { + type: 'string', + const: 'failed', + }, + operationId: { type: 'string', + format: 'uuid', }, - version: { + through: { type: 'integer', - format: 'uint32', + format: 'uint64', minimum: 0, }, - transport: { - $ref: '#/$defs/Transport', - }, - digest: { - type: 'string', + reason: { + $ref: '#/$defs/AgentRecapFailure', }, }, additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], + required: ['kind', 'operationId', 'through', 'reason'], }, - Transport: { + ], + $defs: { + AgentRecapFailure: { type: 'string', - enum: ['local', 'remote'], + enum: ['empty_or_oversized_output', 'incomplete_output', 'model_unavailable'], }, - RecoveryProtocol: { + }, + }, + resources: {}, + }, + gitDiff: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentGitDiffRead', + type: 'object', + properties: { + sessionId: { type: 'string', - enum: ['journal_v1'], + }, + area: { + $ref: '#/$defs/GitArea', + }, + path: { + $ref: '#/$defs/WorkspacePath', + }, + }, + additionalProperties: false, + required: ['sessionId', 'area', 'path'], + $defs: { + GitArea: { + type: 'string', + enum: ['staged', 'worktree', 'untracked'], + }, + WorkspacePath: { + type: 'object', + properties: { + bytesBase64: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['bytesBase64'], }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentGoalResult', + title: 'Nullable_GitDiff', anyOf: [ { - $ref: '#/$defs/AgentGoalResult', + $ref: '#/$defs/GitDiff', }, { type: 'null', }, ], $defs: { - AgentGoalResult: { + GitDiff: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'committed', + const: 'text', }, - goalId: { + patch: { type: 'string', - format: 'uuid', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'goalId', 'revision'], + required: ['kind', 'patch'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'conflict', - }, - actual: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, + const: 'binary', }, }, - additionalProperties: false, required: ['kind'], + additionalProperties: false, }, { type: 'object', properties: { kind: { type: 'string', - const: 'rejected', + const: 'unchanged', }, - reason: { + }, + required: ['kind'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + kind: { type: 'string', + const: 'unsupported', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'reason'], }, ], }, @@ -6666,647 +6754,664 @@ export const contracts = { }, resources: {}, }, - history: { + gitStatus: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryRead', + title: 'AgentSessionRead', type: 'object', properties: { sessionId: { type: 'string', minLength: 1, - }, - through: { - type: ['integer', 'null'], - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - position: { - $ref: '#/$defs/HistoryPosition', - }, - includeThinking: { - type: 'boolean', - default: false, - }, - view: { - $ref: '#/$defs/HistoryView', - default: 'text', - }, - }, - additionalProperties: false, - required: ['sessionId', 'position'], - $defs: { - HistoryPosition: { - description: - 'Row reveals an exact field; Before/After continue at byte boundaries and\nEvent addresses the raw event log. Returned chunks retain ascending presentation\norder, including a reverse window.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'tail', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: - 'Reveal this exact row field from its beginning, including empty tool rows.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'row', - }, - position: { - $ref: '#/$defs/HistoryRowPosition', - }, - field: { - $ref: '#/$defs/HistoryField', - }, - }, - additionalProperties: false, - required: ['kind', 'position', 'field'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before', - }, - cursor: { - $ref: '#/$defs/Cursor', - }, - }, - additionalProperties: false, - required: ['kind', 'cursor'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'after', - }, - cursor: { - $ref: '#/$defs/Cursor', - }, - }, - additionalProperties: false, - required: ['kind', 'cursor'], + }, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_GitStatus', + anyOf: [ + { + $ref: '#/$defs/GitStatus', + }, + { + type: 'null', + }, + ], + $defs: { + GitStatus: { + type: 'object', + properties: { + head: { + type: ['string', 'null'], }, - { - description: - 'Locate canonical output at an execution event, including sparse rows.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'event', - }, - sequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, + entries: { + type: 'array', + items: { + $ref: '#/$defs/GitStatusEntry', }, - additionalProperties: false, - required: ['kind', 'sequence'], }, - ], + }, + additionalProperties: false, + required: ['head', 'entries'], }, - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + GitStatusEntry: { type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + area: { + $ref: '#/$defs/GitArea', }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + path: { + $ref: '#/$defs/WorkspacePath', + }, + kind: { + $ref: '#/$defs/GitChangeKind', }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['area', 'path', 'kind'], }, - HistoryField: { - description: 'A readable field in a conversation window, in presentation order.', + GitArea: { type: 'string', - enum: ['thinking', 'body'], + enum: ['staged', 'worktree', 'untracked'], }, - Cursor: { - description: - "Byte offset in one message field's UTF-8 text, not a bearer capability.", + WorkspacePath: { type: 'object', properties: { - field: { - $ref: '#/$defs/HistoryField', - default: 'body', - }, - position: { - $ref: '#/$defs/HistoryRowPosition', - }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + bytesBase64: { + type: 'string', }, }, additionalProperties: false, - required: ['position', 'offset'], + required: ['bytesBase64'], }, - HistoryView: { - description: - 'Tool output is available through source reads in both views. Conversation\nwindows carry tool metadata only, keeping large results out of chat paging.', + GitChangeKind: { type: 'string', - enum: ['text', 'conversation'], + enum: ['added', 'modified', 'deleted', 'type_changed', 'conflicted'], }, }, }, + resources: {}, + }, + globalInstructions: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentGlobalInstructionsRead', + type: 'object', + additionalProperties: false, + }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryPage', + title: 'AgentGlobalInstructionsView', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'ready', + const: 'available', }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, + source: { + $ref: '#/$defs/AgentGlobalInstructionSource', }, - chunks: { - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryChunk', - }, + path: { + type: 'string', }, - before: { - anyOf: [ - { - $ref: '#/$defs/Cursor', - }, - { - type: 'null', - }, - ], + revision: { + type: 'string', }, - after: { - anyOf: [ - { - $ref: '#/$defs/Cursor', - }, - { - type: 'null', - }, - ], + content: { + type: 'string', + }, + exists: { + type: 'boolean', }, }, additionalProperties: false, - required: ['kind', 'through', 'chunks', 'before', 'after'], + required: ['kind', 'source', 'path', 'revision', 'content', 'exists'], }, - ], - $defs: { - AgentHistoryChunk: { + { type: 'object', properties: { - modelPart: { - anyOf: [ - { - $ref: '#/$defs/ModelPartIdentity', - }, - { - type: 'null', - }, - ], - }, - hasReferences: { - description: - 'The original user input has quotes, directory references or inline references.\nPresent only on the first fragment; display text cannot reconstruct these references.', - type: 'boolean', - }, - finalOutput: { - description: - "Part of the successfully completed native Turn's final output at `through`.\nIncludes all body parts and source citations; never a Thinking field.", - type: 'boolean', - }, - model: { - anyOf: [ - { - $ref: '#/$defs/HistoryModel', - }, - { - type: 'null', - }, - ], - }, - origin: { - anyOf: [ - { - $ref: '#/$defs/MessageOrigin', - }, - { - type: 'null', - }, - ], - }, - compaction: { - anyOf: [ - { - $ref: '#/$defs/HistoryCompaction', - }, - { - type: 'null', - }, - ], - }, - attachments: { - description: - 'Display metadata only, on the first user-message fragment; no storage capability.', - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryAttachment', - }, + kind: { + type: 'string', + const: 'unavailable', }, - timing: { - anyOf: [ - { - $ref: '#/$defs/HistoryTurnTiming', - }, - { - type: 'null', - }, - ], + }, + required: ['kind'], + additionalProperties: false, + }, + ], + $defs: { + AgentGlobalInstructionSource: { + type: 'string', + enum: ['maka', 'agents'], + }, + }, + }, + resources: {}, + }, + goal: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentGoalRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentGoalView', + type: 'object', + properties: { + enabled: { + type: 'boolean', + }, + current: { + anyOf: [ + { + $ref: '#/$defs/AgentGoalDetail', }, - field: { - $ref: '#/$defs/HistoryField', + { + type: 'null', }, - messageId: { + ], + }, + }, + additionalProperties: false, + required: ['enabled'], + $defs: { + AgentGoalDetail: { + type: 'object', + properties: { + id: { type: 'string', + format: 'uuid', }, - turnId: { + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + objective: { type: 'string', }, - timestamp: { + status: { + $ref: '#/$defs/GoalStatus', + }, + iterations: { type: 'integer', format: 'uint64', minimum: 0, }, - role: { - $ref: '#/$defs/Role', + tokenBudget: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, }, - order: { - description: - 'Stable display order; position remains the canonical source read address.', - $ref: '#/$defs/HistoryRowPosition', + consumed: { + $ref: '#/$defs/GoalMeter', }, - position: { - $ref: '#/$defs/HistoryRowPosition', + remainingTokens: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, }, - offset: { + elapsedSeconds: { type: 'integer', format: 'uint64', minimum: 0, }, - totalBytes: { + retryAt: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + createdAt: { type: 'integer', format: 'uint64', minimum: 0, }, - text: { - type: 'string', + pending: { + type: 'boolean', }, - tool: { + report: { anyOf: [ { - $ref: '#/$defs/HistoryTool', + $ref: '#/$defs/GoalReport', }, { type: 'null', }, ], }, + note: { + type: 'string', + }, }, additionalProperties: false, required: [ - 'field', - 'messageId', - 'turnId', - 'timestamp', - 'role', - 'order', - 'position', - 'offset', - 'totalBytes', - 'text', - 'tool', + 'id', + 'revision', + 'objective', + 'status', + 'iterations', + 'consumed', + 'elapsedSeconds', + 'createdAt', + 'pending', + 'note', ], }, - ModelPartIdentity: { + GoalStatus: { + type: 'string', + enum: [ + 'armed', + 'active', + 'paused', + 'achieved', + 'cancelled', + 'budget_limited', + 'blocked', + ], + }, + GoalMeter: { type: 'object', properties: { - stepId: { + known: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + missing: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + required: ['known', 'missing'], + }, + GoalReport: { + type: 'object', + properties: { + status: { + $ref: '#/$defs/GoalReportKind', + }, + note: { type: 'string', }, - ordinal: { + }, + additionalProperties: false, + required: ['status', 'note'], + }, + GoalReportKind: { + type: 'string', + enum: ['progress', 'achieved', 'blocked', 'paused'], + }, + }, + }, + resources: {}, + }, + goalReceipt: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentGoalReceiptRead', + type: 'object', + properties: { + reference: { + $ref: '#/$defs/RecoveryRef', + }, + }, + additionalProperties: false, + required: ['reference'], + $defs: { + RecoveryRef: { + type: 'object', + properties: { + source: { + $ref: '#/$defs/DeploymentRef', + }, + namespace: { + $ref: '#/$defs/InstanceId', + }, + export: { + type: 'string', + }, + method: { + type: 'string', + }, + operation: { + type: 'string', + format: 'uuid', + }, + fingerprint: { + type: 'string', + }, + contract: { + $ref: '#/$defs/RecoveryContract', + }, + recovery: { + $ref: '#/$defs/RecoveryProtocol', + }, + }, + additionalProperties: false, + required: [ + 'source', + 'namespace', + 'export', + 'method', + 'operation', + 'fingerprint', + 'contract', + 'recovery', + ], + }, + DeploymentRef: { + type: 'object', + properties: { + stateDomain: { + type: 'string', + }, + deployment: { + type: 'string', + format: 'uuid', + }, + revision: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: ['stepId', 'ordinal'], + required: ['stateDomain', 'deployment', 'revision'], + }, + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + RecoveryContract: { + description: + 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + type: 'object', + properties: { + name: { + type: 'string', + }, + version: { + type: 'integer', + format: 'uint32', + minimum: 0, + }, + transport: { + $ref: '#/$defs/Transport', + }, + digest: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['name', 'version', 'transport', 'digest'], + }, + Transport: { + type: 'string', + enum: ['local', 'remote'], + }, + RecoveryProtocol: { + type: 'string', + enum: ['journal_v1'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentGoalResult', + anyOf: [ + { + $ref: '#/$defs/AgentGoalResult', }, - HistoryModel: { - description: 'Model identity recorded for the exact historical assistant step.', + { + type: 'null', + }, + ], + $defs: { + AgentGoalResult: { oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'native', + const: 'committed', }, - requestedModelId: { + goalId: { type: 'string', + format: 'uuid', }, - reportedModelId: { - type: ['string', 'null'], + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'requestedModelId', 'reportedModelId'], + required: ['kind', 'goalId', 'revision'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'imported', + const: 'conflict', }, - modelId: { + actual: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', + }, + reason: { type: 'string', }, }, additionalProperties: false, - required: ['kind', 'modelId'], + required: ['kind', 'reason'], }, ], }, - MessageOrigin: { + }, + }, + resources: {}, + }, + history: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + through: { + type: ['integer', 'null'], + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + position: { + $ref: '#/$defs/HistoryPosition', + }, + includeThinking: { + type: 'boolean', + default: false, + }, + view: { + $ref: '#/$defs/HistoryView', + default: 'text', + }, + }, + additionalProperties: false, + required: ['sessionId', 'position'], + $defs: { + HistoryPosition: { description: - 'Trusted producer attribution, preserved independently of editable content.', + 'Row reveals an exact field; Before/After continue at byte boundaries and\nEvent addresses the raw event log. Returned chunks retain ascending presentation\norder, including a reverse window.', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'session', - }, - invocation: { - $ref: '#/$defs/Invocation', + const: 'start', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'invocation'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'goal', - }, - goalId: { - type: 'string', - }, - objective: { - type: 'string', + const: 'tail', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'goalId', 'objective'], }, { + description: + 'Reveal this exact row field from its beginning, including empty tool rows.', type: 'object', properties: { kind: { type: 'string', - const: 'workhub_task', + const: 'row', }, - taskId: { - type: 'string', + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + field: { + $ref: '#/$defs/HistoryField', }, }, additionalProperties: false, - required: ['kind', 'taskId'], + required: ['kind', 'position', 'field'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'workhub_graph', + const: 'before', }, - graphId: { - type: 'string', + cursor: { + $ref: '#/$defs/Cursor', }, }, additionalProperties: false, - required: ['kind', 'graphId'], + required: ['kind', 'cursor'], }, { - description: 'A worker conversation created by the Bot finished a run.', type: 'object', properties: { kind: { type: 'string', - const: 'bot_worker', + const: 'after', }, - sessionId: { - type: 'string', + cursor: { + $ref: '#/$defs/Cursor', }, }, additionalProperties: false, - required: ['kind', 'sessionId'], + required: ['kind', 'cursor'], }, { description: - "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", + 'Locate canonical output at an execution event, including sparse rows.', type: 'object', properties: { kind: { type: 'string', - const: 'plan', - }, - executionId: { - type: 'string', + const: 'event', }, - title: { - type: 'string', + sequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['kind', 'executionId', 'title'], + required: ['kind', 'sequence'], }, ], }, - Invocation: { - type: 'object', - properties: { - session_id: { - type: 'string', - }, - turn_id: { - type: 'string', - }, - run_id: { - type: 'string', - }, - invocation_id: { - type: 'string', - }, - }, - required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], - }, - HistoryCompaction: { - description: - 'Duration of a successfully adopted context checkpoint, in recorded milliseconds.', - type: 'object', - properties: { - durationMs: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['durationMs'], - }, - AgentHistoryAttachment: { - type: 'object', - properties: { - name: { - type: 'string', - }, - mimeType: { - type: 'string', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - kind: { - $ref: '#/$defs/AttachmentKind', - }, - }, - additionalProperties: false, - required: ['name', 'mimeType', 'bytes', 'kind'], - }, - AttachmentKind: { - type: 'string', - enum: ['image', 'pdf', 'doc', 'code', 'other'], - }, - HistoryTurnTiming: { + HistoryRowPosition: { description: - 'Recorded logical Turn boundaries at the same history fence. An unfinished\nhistorical boundary alone does not establish that execution is still live.', + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - startedAt: { + sourceSequence: { type: 'integer', format: 'uint64', - minimum: 0, + minimum: 1, maximum: 9007199254740991, }, - finished: { - anyOf: [ - { - $ref: '#/$defs/HistoryTurnFinished', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['startedAt', 'finished'], - }, - HistoryTurnFinished: { - type: 'object', - properties: { - at: { + itemIndex: { type: 'integer', format: 'uint64', minimum: 0, maximum: 9007199254740991, }, - outcome: { - $ref: '#/$defs/HistoryTurnOutcome', - }, }, additionalProperties: false, - required: ['at', 'outcome'], - }, - HistoryTurnOutcome: { - type: 'string', - enum: ['completed', 'failed', 'aborted'], + required: ['sourceSequence', 'itemIndex'], }, HistoryField: { description: 'A readable field in a conversation window, in presentation order.', type: 'string', enum: ['thinking', 'body'], }, - Role: { - description: - 'Canonical message author/kind; the field identifies body versus thinking.\nTool body JSON contributes string values, never its keys.', - type: 'string', - enum: ['compaction', 'failure', 'user', 'assistant', 'tool_call', 'tool_result'], - }, - HistoryRowPosition: { + Cursor: { description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + "Byte offset in one message field's UTF-8 text, not a bearer capability.", type: 'object', properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, + field: { + $ref: '#/$defs/HistoryField', + default: 'body', }, - itemIndex: { + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + offset: { type: 'integer', format: 'uint64', minimum: 0, @@ -7314,30 +7419,52 @@ export const contracts = { }, }, additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], + required: ['position', 'offset'], }, - HistoryTool: { + HistoryView: { description: - 'Canonical references at the requested fence, never execution authority.\nBoth references are present only for a unique same-Turn call/result pair\nwith matching origin and parentage. An unpaired row retains its own reference.', + 'Tool output is available through source reads in both views. Conversation\nwindows carry tool metadata only, keeping large results out of chat paging.', + type: 'string', + enum: ['text', 'conversation'], + }, + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentHistoryPage', + oneOf: [ + { type: 'object', properties: { - toolUseId: { + kind: { type: 'string', + const: 'ready', }, - call: { + through: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + chunks: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryChunk', + }, + }, + before: { anyOf: [ { - $ref: '#/$defs/HistoryToolCall', + $ref: '#/$defs/Cursor', }, { type: 'null', }, ], }, - result: { + after: { anyOf: [ { - $ref: '#/$defs/HistoryToolResult', + $ref: '#/$defs/Cursor', }, { type: 'null', @@ -7346,763 +7473,688 @@ export const contracts = { }, }, additionalProperties: false, - required: ['toolUseId', 'call', 'result'], + required: ['kind', 'through', 'chunks', 'before', 'after'], }, - HistoryToolCall: { + ], + $defs: { + AgentHistoryChunk: { type: 'object', properties: { - ancestorToolUseIds: { - description: - 'Recorded ancestors in this Turn, nearest first; display references only.', - type: 'array', - items: { - type: 'string', - }, - }, - origin: { - $ref: '#/$defs/HistoryToolCallOrigin', - }, - presentation: { - description: - 'Opaque display contract captured at dispatch; no execution authority.', - type: ['string', 'null'], - default: null, - }, - position: { - $ref: '#/$defs/HistoryRowPosition', - }, - name: { - type: 'string', - }, - title: { + modelPart: { anyOf: [ { - $ref: '#/$defs/DisplayText', + $ref: '#/$defs/ModelPartIdentity', }, { type: 'null', }, ], }, - pending: { + hasReferences: { description: - 'This native Turn is still open at the history fence; not execution authority.', + 'The original user input has quotes, directory references or inline references.\nPresent only on the first fragment; display text cannot reconstruct these references.', type: 'boolean', }, - dispatched: { + finalOutput: { description: - 'A unique original Host dispatch is recorded at this fence; not liveness.', + "Part of the successfully completed native Turn's final output at `through`.\nIncludes all body parts and source citations; never a Thinking field.", type: 'boolean', }, - }, - additionalProperties: false, - required: [ - 'ancestorToolUseIds', - 'origin', - 'position', - 'name', - 'title', - 'pending', - 'dispatched', - ], - }, - HistoryToolCallOrigin: { - description: - "Who made a call: a model step, a script the model wrote, a tool's own work\nthrough the Host SDK, or a foreign record.", - type: 'string', - enum: ['provider', 'code_mode', 'host_sdk', 'imported'], - }, - DisplayText: { - description: - 'Presentation text is distinct from the stable identity used for navigation.', - type: 'object', - properties: { - fallback: { - type: 'string', - }, - translations: { - type: 'object', - additionalProperties: { - type: 'string', - }, - default: {}, - }, - }, - additionalProperties: false, - required: ['fallback'], - }, - HistoryToolResult: { - type: 'object', - properties: { - settlement: { + model: { anyOf: [ { - $ref: '#/$defs/ToolSettlement', + $ref: '#/$defs/HistoryModel', }, { type: 'null', }, ], }, - position: { - $ref: '#/$defs/HistoryRowPosition', - }, - isError: { - description: - 'A failed or unknown effect, or a returned result that reports failure.', - type: 'boolean', + origin: { + anyOf: [ + { + $ref: '#/$defs/MessageOrigin', + }, + { + type: 'null', + }, + ], }, - image: { - description: - 'Canonical content is an image; this does not grant artifact reads.', - type: 'boolean', + compaction: { + anyOf: [ + { + $ref: '#/$defs/HistoryCompaction', + }, + { + type: 'null', + }, + ], }, - images: { + attachments: { description: - 'Images the model received beside non-image content, readable by index.', - type: 'integer', - format: 'uint32', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['settlement', 'position', 'isError', 'image', 'images'], - }, - ToolSettlement: { - description: - 'Recorded Host settlement for presentation; returned is not business success.\nForeign/provider results do not establish this Host evidence.', - type: 'string', - enum: ['returned', 'failed', 'unknown', 'rejected'], - }, - Cursor: { - description: - "Byte offset in one message field's UTF-8 text, not a bearer capability.", - type: 'object', - properties: { - field: { - $ref: '#/$defs/HistoryField', - default: 'body', - }, - position: { - $ref: '#/$defs/HistoryRowPosition', - }, - offset: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + 'Display metadata only, on the first user-message fragment; no storage capability.', + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryAttachment', + }, }, - }, - additionalProperties: false, - required: ['position', 'offset'], - }, - }, - }, - resources: {}, - }, - historyCheckpointHead: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionRead', - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['sessionId'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentHistoryCheckpointHead', - anyOf: [ - { - $ref: '#/$defs/AgentHistoryCheckpointHead', - }, - { - type: 'null', - }, - ], - $defs: { - AgentHistoryCheckpointHead: { - type: 'object', - properties: { - revision: { - type: 'string', - format: 'uuid', + timing: { + anyOf: [ + { + $ref: '#/$defs/HistoryTurnTiming', + }, + { + type: 'null', + }, + ], }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', + field: { + $ref: '#/$defs/HistoryField', }, - }, - additionalProperties: false, - required: ['revision', 'manifest'], - }, - AgentHistoryCheckpointObject: { - type: 'object', - properties: { - digest: { + messageId: { type: 'string', - pattern: '^[a-f0-9]{64}$', }, - bytes: { + turnId: { + type: 'string', + }, + timestamp: { type: 'integer', format: 'uint64', minimum: 0, - maximum: 9007199254740991, }, - }, - additionalProperties: false, - required: ['digest', 'bytes'], - }, - }, - }, - resources: {}, - }, - historyCheckpointLoadReceipt: { - mode: 'query', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCheckpointLoadReceiptRead', - type: 'object', - properties: { - reference: { - $ref: '#/$defs/RecoveryRef', - }, - input: { - $ref: '#/$defs/AgentHistoryCheckpointRead', - }, - }, - additionalProperties: false, - required: ['reference', 'input'], - $defs: { - RecoveryRef: { - type: 'object', - properties: { - source: { - $ref: '#/$defs/DeploymentRef', + role: { + $ref: '#/$defs/Role', }, - namespace: { - $ref: '#/$defs/InstanceId', + order: { + description: + 'Stable display order; position remains the canonical source read address.', + $ref: '#/$defs/HistoryRowPosition', }, - export: { - type: 'string', + position: { + $ref: '#/$defs/HistoryRowPosition', }, - method: { - type: 'string', + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - operation: { - type: 'string', - format: 'uuid', + totalBytes: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - fingerprint: { + text: { type: 'string', }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + tool: { + anyOf: [ + { + $ref: '#/$defs/HistoryTool', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', + 'field', + 'messageId', + 'turnId', + 'timestamp', + 'role', + 'order', + 'position', + 'offset', + 'totalBytes', + 'text', + 'tool', ], }, - DeploymentRef: { + ModelPartIdentity: { type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + stepId: { type: 'string', - format: 'uuid', }, - revision: { + ordinal: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + required: ['stepId', 'ordinal'], }, - RecoveryContract: { - description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', - type: 'object', - properties: { - name: { - type: 'string', - }, - version: { - type: 'integer', - format: 'uint32', - minimum: 0, - }, - transport: { - $ref: '#/$defs/Transport', + HistoryModel: { + description: 'Model identity recorded for the exact historical assistant step.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'native', + }, + requestedModelId: { + type: 'string', + }, + reportedModelId: { + type: ['string', 'null'], + }, + }, + additionalProperties: false, + required: ['kind', 'requestedModelId', 'reportedModelId'], }, - digest: { - type: 'string', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'imported', + }, + modelId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'modelId'], }, - }, - additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], - }, - Transport: { - type: 'string', - enum: ['local', 'remote'], - }, - RecoveryProtocol: { - type: 'string', - enum: ['journal_v1'], + ], }, - AgentHistoryCheckpointRead: { - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, + MessageOrigin: { + description: + 'Trusted producer attribution, preserved independently of editable content.', + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'session', + }, + invocation: { + $ref: '#/$defs/Invocation', + }, + }, + additionalProperties: false, + required: ['kind', 'invocation'], }, - revision: { - description: - 'Only the exact currently retained revision is readable; no latest fallback.', - type: 'string', - format: 'uuid', + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'goal', + }, + goalId: { + type: 'string', + }, + objective: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'goalId', 'objective'], }, - }, - additionalProperties: false, - required: ['sessionId', 'revision'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentCheckpointLoadReceipt', - type: 'object', - properties: { - operation: { - anyOf: [ { - $ref: '#/$defs/OperationObservation', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workhub_task', + }, + taskId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'taskId'], }, { - type: 'null', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'workhub_graph', + }, + graphId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'graphId'], }, - ], - }, - load: { - anyOf: [ { - $ref: '#/$defs/AgentCheckpointLoad', + description: 'A worker conversation created by the Bot finished a run.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'bot_worker', + }, + sessionId: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'sessionId'], }, { - type: 'null', + description: + "The opening message of an approved Plan execution. `title` is the\napproved Plan's title when the execution was frozen, not the current Plan.", + type: 'object', + properties: { + kind: { + type: 'string', + const: 'plan', + }, + executionId: { + type: 'string', + }, + title: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['kind', 'executionId', 'title'], }, ], }, - }, - additionalProperties: false, - $defs: { - OperationObservation: { - type: 'object', - properties: { - reference: { - $ref: '#/$defs/RecoveryRef', - }, - outcome: { - $ref: '#/$defs/OperationOutcome', - }, - cleanup: { - $ref: '#/$defs/OperationCleanup', - }, - }, - additionalProperties: false, - required: ['reference', 'outcome', 'cleanup'], - }, - RecoveryRef: { + Invocation: { type: 'object', properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - export: { + session_id: { type: 'string', }, - method: { + turn_id: { type: 'string', }, - operation: { + run_id: { type: 'string', - format: 'uuid', }, - fingerprint: { + invocation_id: { type: 'string', }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + }, + required: ['session_id', 'turn_id', 'run_id', 'invocation_id'], + }, + HistoryCompaction: { + description: + 'Duration of a successfully adopted context checkpoint, in recorded milliseconds.', + type: 'object', + properties: { + durationMs: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['durationMs'], }, - DeploymentRef: { + AgentHistoryAttachment: { type: 'object', properties: { - stateDomain: { + name: { type: 'string', }, - deployment: { + mimeType: { type: 'string', - format: 'uuid', }, - revision: { + bytes: { type: 'integer', format: 'uint64', minimum: 0, }, + kind: { + $ref: '#/$defs/AttachmentKind', + }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], + required: ['name', 'mimeType', 'bytes', 'kind'], }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + AttachmentKind: { + type: 'string', + enum: ['image', 'pdf', 'doc', 'code', 'other'], }, - RecoveryContract: { + HistoryTurnTiming: { description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + 'Recorded logical Turn boundaries at the same history fence. An unfinished\nhistorical boundary alone does not establish that execution is still live.', type: 'object', properties: { - name: { - type: 'string', - }, - version: { + startedAt: { type: 'integer', - format: 'uint32', + format: 'uint64', minimum: 0, + maximum: 9007199254740991, }, - transport: { - $ref: '#/$defs/Transport', + finished: { + anyOf: [ + { + $ref: '#/$defs/HistoryTurnFinished', + }, + { + type: 'null', + }, + ], }, - digest: { - type: 'string', + }, + additionalProperties: false, + required: ['startedAt', 'finished'], + }, + HistoryTurnFinished: { + type: 'object', + properties: { + at: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + outcome: { + $ref: '#/$defs/HistoryTurnOutcome', }, }, additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], + required: ['at', 'outcome'], }, - Transport: { + HistoryTurnOutcome: { type: 'string', - enum: ['local', 'remote'], + enum: ['completed', 'failed', 'aborted'], }, - RecoveryProtocol: { + HistoryField: { + description: 'A readable field in a conversation window, in presentation order.', type: 'string', - enum: ['journal_v1'], + enum: ['thinking', 'body'], }, - OperationOutcome: { + Role: { description: - 'Unresolved means no durable result is known, including after process loss.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unresolved', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'returned', - }, - value: true, - }, - additionalProperties: false, - required: ['kind', 'value'], + 'Canonical message author/kind; the field identifies body versus thinking.\nTool body JSON contributes string values, never its keys.', + type: 'string', + enum: ['compaction', 'failure', 'user', 'assistant', 'tool_call', 'tool_result'], + }, + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + type: 'object', + properties: { + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unknown', - }, - diagnostic: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'diagnostic'], + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - ], + }, + additionalProperties: false, + required: ['sourceSequence', 'itemIndex'], }, - OperationCleanup: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unconfirmed', - }, - }, - required: ['kind'], - additionalProperties: false, + HistoryTool: { + description: + 'Canonical references at the requested fence, never execution authority.\nBoth references are present only for a unique same-Turn call/result pair\nwith matching origin and parentage. An unpaired row retains its own reference.', + type: 'object', + properties: { + toolUseId: { + type: 'string', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'confirmed', + call: { + anyOf: [ + { + $ref: '#/$defs/HistoryToolCall', }, - }, - required: ['kind'], - additionalProperties: false, + { + type: 'null', + }, + ], }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'failed', + result: { + anyOf: [ + { + $ref: '#/$defs/HistoryToolResult', }, - diagnostic: { - type: 'string', + { + type: 'null', }, - }, - additionalProperties: false, - required: ['kind', 'diagnostic'], + ], }, - ], + }, + additionalProperties: false, + required: ['toolUseId', 'call', 'result'], }, - AgentCheckpointLoad: { + HistoryToolCall: { type: 'object', properties: { - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', + ancestorToolUseIds: { + description: + 'Recorded ancestors in this Turn, nearest first; display references only.', + type: 'array', + items: { + type: 'string', + }, }, - inspection: { - $ref: '#/$defs/AgentCheckpointInspection', + origin: { + $ref: '#/$defs/HistoryToolCallOrigin', }, - state: { - $ref: '#/$defs/AgentCheckpointLoadState', + presentation: { + description: + 'Opaque display contract captured at dispatch; no execution authority.', + type: ['string', 'null'], + default: null, + }, + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + name: { + type: 'string', + }, + title: { + anyOf: [ + { + $ref: '#/$defs/DisplayText', + }, + { + type: 'null', + }, + ], + }, + pending: { + description: + 'This native Turn is still open at the history fence; not execution authority.', + type: 'boolean', + }, + dispatched: { + description: + 'A unique original Host dispatch is recorded at this fence; not liveness.', + type: 'boolean', }, }, additionalProperties: false, - required: ['head', 'inspection', 'state'], + required: [ + 'ancestorToolUseIds', + 'origin', + 'position', + 'name', + 'title', + 'pending', + 'dispatched', + ], }, - AgentHistoryCheckpointHead: { + HistoryToolCallOrigin: { + description: + "Who made a call: a model step, a script the model wrote, a tool's own work\nthrough the Host SDK, or a foreign record.", + type: 'string', + enum: ['provider', 'code_mode', 'host_sdk', 'imported'], + }, + DisplayText: { + description: + 'Presentation text is distinct from the stable identity used for navigation.', type: 'object', properties: { - revision: { + fallback: { type: 'string', - format: 'uuid', }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', + translations: { + type: 'object', + additionalProperties: { + type: 'string', + }, + default: {}, }, }, additionalProperties: false, - required: ['revision', 'manifest'], + required: ['fallback'], }, - AgentHistoryCheckpointObject: { + HistoryToolResult: { type: 'object', properties: { - digest: { - type: 'string', - pattern: '^[a-f0-9]{64}$', + settlement: { + anyOf: [ + { + $ref: '#/$defs/ToolSettlement', + }, + { + type: 'null', + }, + ], }, - bytes: { + position: { + $ref: '#/$defs/HistoryRowPosition', + }, + isError: { + description: + 'A failed or unknown effect, or a returned result that reports failure.', + type: 'boolean', + }, + image: { + description: + 'Canonical content is an image; this does not grant artifact reads.', + type: 'boolean', + }, + images: { + description: + 'Images the model received beside non-image content, readable by index.', type: 'integer', - format: 'uint64', + format: 'uint32', minimum: 0, - maximum: 9007199254740991, }, }, additionalProperties: false, - required: ['digest', 'bytes'], + required: ['settlement', 'position', 'isError', 'image', 'images'], }, - AgentCheckpointInspection: { + ToolSettlement: { + description: + 'Recorded Host settlement for presentation; returned is not business success.\nForeign/provider results do not establish this Host evidence.', + type: 'string', + enum: ['returned', 'failed', 'unknown', 'rejected'], + }, + Cursor: { + description: + "Byte offset in one message field's UTF-8 text, not a bearer capability.", type: 'object', properties: { - sourceSessionId: { - type: 'string', - minLength: 1, - }, - operationId: { - type: 'string', - format: 'uuid', - }, - }, - additionalProperties: false, - required: ['sourceSessionId', 'operationId'], - }, - AgentCheckpointLoadState: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'preparing', - }, - }, - required: ['kind'], - additionalProperties: false, + field: { + $ref: '#/$defs/HistoryField', + default: 'body', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'ready', - }, - sessionIds: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - anyOf: [ - { - $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'sessionIds'], + position: { + $ref: '#/$defs/HistoryRowPosition', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unavailable', - }, - }, - required: ['kind'], - additionalProperties: false, + offset: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - ], + }, + additionalProperties: false, + required: ['position', 'offset'], }, - AgentCheckpointWorkspaceSnapshot: { + }, + }, + resources: {}, + }, + historyCheckpointHead: { + mode: 'query', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSessionRead', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['sessionId'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentHistoryCheckpointHead', + anyOf: [ + { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + { + type: 'null', + }, + ], + $defs: { + AgentHistoryCheckpointHead: { type: 'object', properties: { - identity: { + revision: { type: 'string', + format: 'uuid', }, - consistency: { - $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', - }, - warning: { - $ref: '#/$defs/AgentCheckpointWorkspaceWarning', - }, - inventory: { + manifest: { $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['identity', 'consistency', 'warning', 'inventory'], + required: ['revision', 'manifest'], }, - AgentCheckpointWorkspaceConsistency: { - oneOf: [ - { - description: - 'The caller confirms all other writers remain paused for the entire copy.', - type: 'string', - const: 'caller_confirmed_quiescent', - }, - { - description: - 'Files may not represent one instant; successful digest checks do not change this.', + AgentHistoryCheckpointObject: { + type: 'object', + properties: { + digest: { type: 'string', - const: 'unverified', + pattern: '^[a-f0-9]{64}$', }, - ], - }, - AgentCheckpointWorkspaceWarning: { - oneOf: [ - { - description: - 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', - type: 'string', - const: 'external_writers_not_fenced', + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, - ], + }, + additionalProperties: false, + required: ['digest', 'bytes'], }, }, }, resources: {}, }, - historyCheckpointReceipt: { + historyCheckpointLoadReceipt: { mode: 'query', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointReceiptRead', + title: 'AgentCheckpointLoadReceiptRead', type: 'object', properties: { reference: { $ref: '#/$defs/RecoveryRef', }, input: { - description: - 'The exact original input. Changed input cannot select another publication.', - $ref: '#/$defs/AgentHistoryCheckpointPublish', + $ref: '#/$defs/AgentHistoryCheckpointRead', }, }, additionalProperties: false, @@ -8208,37 +8260,31 @@ export const contracts = { type: 'string', enum: ['journal_v1'], }, - AgentHistoryCheckpointPublish: { + AgentHistoryCheckpointRead: { type: 'object', properties: { sessionId: { type: 'string', minLength: 1, }, - expectedRevision: { - type: ['string', 'null'], - format: 'uuid', - }, - confirmedSubtreeDigest: { + revision: { description: - 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', + 'Only the exact currently retained revision is readable; no latest fallback.', type: 'string', - pattern: '^[a-f0-9]{64}$', + format: 'uuid', }, }, additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], + required: ['sessionId', 'revision'], }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointReceipt', + title: 'AgentCheckpointLoadReceipt', type: 'object', properties: { operation: { - description: - 'Original Host observation. Domain success never fabricates Host cleanup confirmation.', anyOf: [ { $ref: '#/$defs/OperationObservation', @@ -8248,12 +8294,10 @@ export const contracts = { }, ], }, - publication: { - description: - 'Authoritative domain result even when the Host response was lost.\nAbsence is not permission to recapture under a replacement operation.', + load: { anyOf: [ { - $ref: '#/$defs/AgentHistoryCheckpointPublication', + $ref: '#/$defs/AgentCheckpointLoad', }, { type: 'null', @@ -8462,83 +8506,107 @@ export const contracts = { }, ], }, - AgentHistoryCheckpointPublication: { + AgentCheckpointLoad: { type: 'object', properties: { + head: { + $ref: '#/$defs/AgentHistoryCheckpointHead', + }, + inspection: { + $ref: '#/$defs/AgentCheckpointInspection', + }, state: { - $ref: '#/$defs/AgentHistoryCheckpointState', + $ref: '#/$defs/AgentCheckpointLoadState', }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', + }, + additionalProperties: false, + required: ['head', 'inspection', 'state'], + }, + AgentHistoryCheckpointHead: { + type: 'object', + properties: { + revision: { + type: 'string', + format: 'uuid', + }, + manifest: { + $ref: '#/$defs/AgentHistoryCheckpointObject', }, }, additionalProperties: false, - required: ['state', 'cleanupPending'], + required: ['revision', 'manifest'], }, - AgentHistoryCheckpointState: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'capturing', - }, - }, - required: ['kind'], - additionalProperties: false, + AgentHistoryCheckpointObject: { + type: 'object', + properties: { + digest: { + type: 'string', + pattern: '^[a-f0-9]{64}$', }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'prepared', - }, - }, - required: ['kind'], - additionalProperties: false, + bytes: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['digest', 'bytes'], + }, + AgentCheckpointInspection: { + type: 'object', + properties: { + sourceSessionId: { + type: 'string', + minLength: 1, + }, + operationId: { + type: 'string', + format: 'uuid', }, + }, + additionalProperties: false, + required: ['sourceSessionId', 'operationId'], + }, + AgentCheckpointLoadState: { + oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'published', - }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', + const: 'preparing', }, }, + required: ['kind'], additionalProperties: false, - required: ['kind', 'head'], }, { type: 'object', properties: { kind: { type: 'string', - const: 'conflict', + const: 'ready', }, - actual: { - type: ['string', 'null'], - format: 'uuid', + sessionIds: { + type: 'array', + items: { + type: 'string', + }, }, - }, - additionalProperties: false, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'interrupted', + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + { + type: 'null', + }, + ], }, }, - required: ['kind'], additionalProperties: false, + required: ['kind', 'sessionIds'], }, { type: 'object', @@ -8553,36 +8621,50 @@ export const contracts = { }, ], }, - AgentHistoryCheckpointHead: { + AgentCheckpointWorkspaceSnapshot: { type: 'object', properties: { - revision: { + identity: { type: 'string', - format: 'uuid', }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + inventory: { + $ref: '#/$defs/AgentHistoryCheckpointObject', + }, + }, + additionalProperties: false, + required: ['identity', 'consistency', 'warning', 'inventory'], + }, + AgentCheckpointWorkspaceConsistency: { + oneOf: [ + { + description: + 'The caller confirms all other writers remain paused for the entire copy.', + type: 'string', + const: 'caller_confirmed_quiescent', + }, + { + description: + 'Files may not represent one instant; successful digest checks do not change this.', + type: 'string', + const: 'unverified', }, - }, - additionalProperties: false, - required: ['revision', 'manifest'], + ], }, - AgentHistoryCheckpointObject: { - type: 'object', - properties: { - digest: { + AgentCheckpointWorkspaceWarning: { + oneOf: [ + { + description: + 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, + const: 'external_writers_not_fenced', }, - }, - additionalProperties: false, - required: ['digest', 'bytes'], + ], }, }, }, @@ -12911,7 +12993,7 @@ export const contracts = { resources: {}, }, loadedHistoryCheckpointArtifact: { - mode: 'query', + mode: 'subscription', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', title: 'AgentCheckpointArtifactRead', @@ -17411,11 +17493,11 @@ export const contracts = { }, resources: {}, }, - publishHistoryCheckpoint: { + publishCheckpoint: { mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointPublish', + title: 'AgentCheckpointPublish', type: 'object', properties: { sessionId: { @@ -17432,177 +17514,27 @@ export const contracts = { type: 'string', pattern: '^[a-f0-9]{64}$', }, - }, - additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistoryCheckpointPublication', - type: 'object', - properties: { - state: { - $ref: '#/$defs/AgentHistoryCheckpointState', - }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['state', 'cleanupPending'], - $defs: { - AgentHistoryCheckpointState: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'capturing', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'prepared', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'published', - }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', - }, - }, - additionalProperties: false, - required: ['kind', 'head'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'conflict', - }, - actual: { - type: ['string', 'null'], - format: 'uuid', - }, - }, - additionalProperties: false, - required: ['kind'], - }, + workspaceConsistency: { + description: + 'Null captures history only. Otherwise the caller explicitly selects the\nWorkspace consistency guarantee; other applications are never fenced.', + anyOf: [ { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'interrupted', - }, - }, - required: ['kind'], - additionalProperties: false, + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', }, { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unavailable', - }, - }, - required: ['kind'], - additionalProperties: false, + type: 'null', }, ], }, - AgentHistoryCheckpointHead: { - type: 'object', - properties: { - revision: { - type: 'string', - format: 'uuid', - }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', - }, - }, - additionalProperties: false, - required: ['revision', 'manifest'], - }, - AgentHistoryCheckpointObject: { - type: 'object', - properties: { - digest: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['digest', 'bytes'], - }, - }, - }, - resources: {}, - }, - publishSessionCheckpoint: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionCheckpointPublish', - type: 'object', - properties: { - history: { - $ref: '#/$defs/AgentHistoryCheckpointPublish', - }, - workspaceConsistency: { - description: 'Must be explicitly selected. Maka does not fence other applications.', - $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', - }, }, additionalProperties: false, - required: ['history', 'workspaceConsistency'], + required: [ + 'sessionId', + 'expectedRevision', + 'confirmedSubtreeDigest', + 'workspaceConsistency', + ], $defs: { - AgentHistoryCheckpointPublish: { - type: 'object', - properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - expectedRevision: { - type: ['string', 'null'], - format: 'uuid', - }, - confirmedSubtreeDigest: { - description: - 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - }, - additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], - }, AgentCheckpointWorkspaceConsistency: { oneOf: [ { @@ -17623,37 +17555,30 @@ export const contracts = { }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionCheckpointPublication', + title: 'AgentCheckpointPublication', type: 'object', properties: { - publication: { - $ref: '#/$defs/AgentHistoryCheckpointPublication', + state: { + $ref: '#/$defs/AgentHistoryCheckpointState', }, - workspaceConsistency: { - $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + cleanupPending: { + description: 'Cleanup failure does not undo a successful publication.', + type: 'boolean', }, - warning: { - description: 'Stable warning, present even with a caller attestation.', - $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceNotice', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['publication', 'workspaceConsistency', 'warning'], + required: ['state', 'cleanupPending'], $defs: { - AgentHistoryCheckpointPublication: { - type: 'object', - properties: { - state: { - $ref: '#/$defs/AgentHistoryCheckpointState', - }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', - }, - }, - additionalProperties: false, - required: ['state', 'cleanupPending'], - }, AgentHistoryCheckpointState: { oneOf: [ { @@ -17762,6 +17687,19 @@ export const contracts = { additionalProperties: false, required: ['digest', 'bytes'], }, + AgentCheckpointWorkspaceNotice: { + type: 'object', + properties: { + consistency: { + $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + }, + warning: { + $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + }, + }, + additionalProperties: false, + required: ['consistency', 'warning'], + }, AgentCheckpointWorkspaceConsistency: { oneOf: [ { @@ -18319,40 +18257,16 @@ export const contracts = { }, ], $defs: { - AgentHistoryCheckpointFrame: { - oneOf: [ - { - description: - 'The streamed payload is history; Workspace is a separate manifest component.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'native_session_v1', - }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', - }, - inventory: { - $ref: '#/$defs/AgentHistoryCheckpointInventory', - }, - bundle: { - $ref: '#/$defs/AgentHistoryCheckpointObject', - }, - workspace: { - $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', - }, - }, - additionalProperties: false, - required: ['kind', 'head', 'inventory', 'bundle', 'workspace'], - }, + AgentHistoryCheckpointFrame: { + oneOf: [ { - description: 'The following bytes use the MAKA-SESSION v2 history format.', + description: + 'MAKA-SESSION v2 history bytes follow; Workspace objects are separate.', type: 'object', properties: { kind: { type: 'string', - const: 'native_history_v1', + const: 'checkpoint', }, head: { $ref: '#/$defs/AgentHistoryCheckpointHead', @@ -18363,6 +18277,16 @@ export const contracts = { bundle: { $ref: '#/$defs/AgentHistoryCheckpointObject', }, + workspace: { + anyOf: [ + { + $ref: '#/$defs/AgentCheckpointWorkspaceSnapshot', + }, + { + type: 'null', + }, + ], + }, }, additionalProperties: false, required: ['kind', 'head', 'inventory', 'bundle'], @@ -20999,7 +20923,10 @@ export const contracts = { $ref: '#/$defs/AgentHistoryCheckpointRead', }, parent: { - $ref: '#/$defs/AgentDirectorySelection', + description: + 'Absolute parent path. The Host captures and validates its directory identity.', + type: 'string', + minLength: 1, }, permissions: { $ref: '#/$defs/AgentWorkspacePermissions', @@ -21037,27 +20964,6 @@ export const contracts = { additionalProperties: false, required: ['sessionId', 'revision'], }, - AgentDirectorySelection: { - type: 'object', - properties: { - rootId: { - type: 'string', - minLength: 1, - }, - segments: { - type: 'array', - items: { - type: 'string', - }, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - }, - additionalProperties: false, - required: ['rootId', 'segments', 'directoryIdentity'], - }, AgentWorkspacePermissions: { type: 'object', properties: { @@ -23145,503 +23051,161 @@ export const contracts = { 'through', 'query', 'caseSensitive', - 'wholeWord', - 'includeInternal', - ], - $defs: { - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', - type: 'object', - properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], - }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentHistorySearchPage', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'ready', - }, - through: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - matches: { - type: 'array', - items: { - $ref: '#/$defs/AgentHistoryMatch', - }, - maxItems: 16, - }, - nextAfter: { - description: - 'Continue even when this batch has no matches. Null is the end of the scan.', - anyOf: [ - { - $ref: '#/$defs/HistoryRowPosition', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'through', 'matches', 'nextAfter'], - }, - ], - $defs: { - AgentHistoryMatch: { - type: 'object', - properties: { - field: { - description: - 'Exact canonical field to read through the source query; history body may omit it.', - $ref: '#/$defs/SourceField', - }, - position: { - description: - 'Exact transcript row. Open with HistoryPosition::Row at the same raw-event\nfence: Thinking for a thinking match, Body for every other source field.', - $ref: '#/$defs/HistoryRowPosition', - }, - preview: { - type: 'string', - maxLength: 384, - }, - }, - additionalProperties: false, - required: ['field', 'position', 'preview'], - }, - SourceField: { - description: - 'An explicitly selected canonical transcript field, never an execution grant.', - oneOf: [ - { - type: 'string', - enum: [ - 'body', - 'thinking', - 'tool_name', - 'failure_message', - 'error_class', - 'arguments', - 'result', - ], - }, - { - description: - 'The original service operation observation, separate from its returned value.', - type: 'string', - const: 'receipt', - }, - ], - }, - HistoryRowPosition: { - description: - 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', - type: 'object', - properties: { - sourceSequence: { - type: 'integer', - format: 'uint64', - minimum: 1, - maximum: 9007199254740991, - }, - itemIndex: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['sourceSequence', 'itemIndex'], - }, - }, - }, - resources: {}, - }, - selectBackend: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSelectBackend', - type: 'object', - properties: { - sessionId: { - type: 'string', - }, - backend: { - $ref: '#/$defs/AgentBackendPreference', - }, - }, - additionalProperties: false, - required: ['sessionId', 'backend'], - $defs: { - AgentBackendPreference: { - description: - "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'model', - }, - connectionId: { - type: 'string', - minLength: 1, - }, - modelId: { - type: 'string', - minLength: 1, - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ThinkingLevel', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - required: ['kind', 'connectionId', 'modelId'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'executor', - }, - selection: { - $ref: '#/$defs/AgentExecutorSelection', - }, - }, - additionalProperties: false, - required: ['kind', 'selection'], - }, - ], - }, - ThinkingLevel: { - type: 'string', - enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], - }, - AgentExecutorSelection: { - type: 'object', - properties: { - executor: { - type: 'string', - minLength: 1, - }, - settings: { - $ref: '#/$defs/ExecutorSettings', - }, - }, - additionalProperties: false, - required: ['executor', 'settings'], - }, - ExecutorSettings: { + 'wholeWord', + 'includeInternal', + ], + $defs: { + HistoryRowPosition: { description: - 'Executor-owned model selection, independent of Host model connections.', + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', type: 'object', properties: { - model: { - type: ['string', 'null'], - }, - thinkingLevel: { - anyOf: [ - { - $ref: '#/$defs/ExecutorEffort', - }, - { - type: 'null', - }, - ], + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, }, - runtime: { - type: ['string', 'null'], + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, }, }, additionalProperties: false, - }, - ExecutorEffort: { - description: "Runtime-native effort, not a Host model's closed reasoning enum.", - type: 'string', + required: ['sourceSequence', 'itemIndex'], }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentMetadataResult', + title: 'AgentHistorySearchPage', oneOf: [ { type: 'object', properties: { kind: { type: 'string', - const: 'committed', - }, - session: { - $ref: '#/$defs/AgentSession', - }, - }, - additionalProperties: false, - required: ['kind', 'session'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'revision_conflict', - }, - expected: { - type: 'integer', - format: 'uint64', - minimum: 0, + const: 'ready', }, - actual: { + through: { type: 'integer', format: 'uint64', minimum: 0, }, - }, - additionalProperties: false, - required: ['kind', 'expected', 'actual'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'rejected', - }, - reason: { - $ref: '#/$defs/AgentRejection', + matches: { + type: 'array', + items: { + $ref: '#/$defs/AgentHistoryMatch', + }, + maxItems: 16, }, - }, - additionalProperties: false, - required: ['kind', 'reason'], - }, - ], - $defs: { - AgentSession: { - type: 'object', - properties: { - parentSessionId: { + nextAfter: { description: - 'Session that initiated creation, independent of fork ancestry and task ownership.', - type: ['string', 'null'], - }, - branch: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionBranch', - }, - { - type: 'null', - }, - ], - }, - backend: { + 'Continue even when this batch has no matches. Null is the end of the scan.', anyOf: [ { - $ref: '#/$defs/AgentBackendPreference', + $ref: '#/$defs/HistoryRowPosition', }, { type: 'null', }, ], }, - tools: { - type: ['array', 'null'], - items: { - type: 'string', - }, - }, - dataSource: { - $ref: '#/$defs/AgentDataSource', - }, - permissions: { - $ref: '#/$defs/AgentWorkspacePermissions', - }, - boundaryRevision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - binding: { - $ref: '#/$defs/BindingStamp', - }, - id: { - type: 'string', - }, - title: { - type: 'string', - }, - labels: { - type: 'array', - items: { - type: 'string', - }, - }, - workspace: { - $ref: '#/$defs/AgentWorkspace', - }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - updatedAt: { - type: 'integer', - format: 'uint64', - minimum: 0, - }, - archived: { - type: 'boolean', - }, - pinned: { - type: 'boolean', - }, - hasUnread: { - type: 'boolean', - }, - tailMessageId: { - description: - 'Exact durable visible tail accepted by markRead; independent of preview clocks.', - type: ['string', 'null'], - }, - activity: { - $ref: '#/$defs/AgentSessionActivity', - }, }, additionalProperties: false, - required: [ - 'parentSessionId', - 'branch', - 'dataSource', - 'permissions', - 'boundaryRevision', - 'binding', - 'id', - 'title', - 'labels', - 'workspace', - 'revision', - 'updatedAt', - 'archived', - 'pinned', - 'hasUnread', - 'tailMessageId', - 'activity', - ], + required: ['kind', 'through', 'matches', 'nextAfter'], }, - AgentSessionBranch: { - description: - 'Frozen conversation ancestry for presentation, not authority over the source.', + ], + $defs: { + AgentHistoryMatch: { type: 'object', properties: { - sessionId: { - type: 'string', - minLength: 1, + field: { + description: + 'Exact canonical field to read through the source query; history body may omit it.', + $ref: '#/$defs/SourceField', }, position: { - $ref: '#/$defs/AgentForkPosition', + description: + 'Exact transcript row. Open with HistoryPosition::Row at the same raw-event\nfence: Thinking for a thinking match, Body for every other source field.', + $ref: '#/$defs/HistoryRowPosition', + }, + preview: { + type: 'string', + maxLength: 384, }, }, additionalProperties: false, - required: ['sessionId', 'position'], - }, - AgentForkPosition: { - description: 'A complete logical Turn boundary within the source session.', - oneOf: [ - { - description: 'Capture an empty history prefix.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'start', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: 'Capture all current history, including later compaction facts.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'end', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - description: 'Include the complete target Turn.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'through_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + required: ['field', 'position', 'preview'], + }, + SourceField: { + description: + 'An explicitly selected canonical transcript field, never an execution grant.', + oneOf: [ + { + type: 'string', + enum: [ + 'body', + 'thinking', + 'tool_name', + 'failure_message', + 'error_class', + 'arguments', + 'result', + ], }, { description: - 'Exclude the complete target Turn, including its submitted inputs.', - type: 'object', - properties: { - kind: { - type: 'string', - const: 'before_turn', - }, - turnId: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['kind', 'turnId'], + 'The original service operation observation, separate from its returned value.', + type: 'string', + const: 'receipt', }, ], }, + HistoryRowPosition: { + description: + 'Disposable presentation locator, never authority or a canonical execution fact.\nThe source is the original emitting fact; the index precedes UI filtering.', + type: 'object', + properties: { + sourceSequence: { + type: 'integer', + format: 'uint64', + minimum: 1, + maximum: 9007199254740991, + }, + itemIndex: { + type: 'integer', + format: 'uint64', + minimum: 0, + maximum: 9007199254740991, + }, + }, + additionalProperties: false, + required: ['sourceSequence', 'itemIndex'], + }, + }, + }, + resources: {}, + }, + selectBackend: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSelectBackend', + type: 'object', + properties: { + sessionId: { + type: 'string', + }, + backend: { + $ref: '#/$defs/AgentBackendPreference', + }, + }, + additionalProperties: false, + required: ['sessionId', 'backend'], + $defs: { AgentBackendPreference: { description: "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", @@ -23737,322 +23301,250 @@ export const contracts = { description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', }, - AgentDataSource: { - description: - 'Stable business-data origin, independent of package or activation revisions.', + }, + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentMetadataResult', + oneOf: [ + { type: 'object', properties: { - stateDomain: { - type: 'string', - }, - deployment: { + kind: { type: 'string', - format: 'uuid', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - }, - additionalProperties: false, - required: ['stateDomain', 'deployment', 'namespace'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, - }, - AgentWorkspacePermissions: { - type: 'object', - properties: { - access: { - $ref: '#/$defs/AgentWorkspaceAccess', + const: 'committed', }, - requestPermissions: { - type: 'boolean', + session: { + $ref: '#/$defs/AgentSession', }, }, additionalProperties: false, - required: ['access', 'requestPermissions'], - }, - AgentWorkspaceAccess: { - description: - 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', - type: 'string', - enum: ['read_only', 'workspace_write', 'danger_full_access'], + required: ['kind', 'session'], }, - BindingStamp: { + { type: 'object', properties: { - id: { + kind: { type: 'string', - format: 'uuid', + const: 'revision_conflict', }, - permissionEpoch: { + expected: { type: 'integer', format: 'uint64', - minimum: 0, - }, - }, - additionalProperties: false, - required: ['id', 'permissionEpoch'], - }, - AgentWorkspace: { - type: 'object', - properties: { - worktree: { - description: - 'Native owner allocation; never accepted from a caller as workspace authority.', - anyOf: [ - { - $ref: '#/$defs/Binding', - }, - { - type: 'null', - }, - ], - }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', - }, - path: { - type: 'string', - minLength: 1, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', - }, - }, - additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], - }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', - type: 'object', - properties: { - id: { - type: 'string', - }, - common_dir: { - type: 'string', - }, - source: { - type: 'string', - }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - base_commit: { - type: 'string', - }, - directory: { - type: 'string', - }, - }, - additionalProperties: false, - required: [ - 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', - ], - }, - AgentSessionActivity: { - type: 'string', - enum: ['idle', 'working', 'waiting'], - }, - AgentRejection: { - type: 'string', - enum: [ - 'permission_change_requires_user', - 'title_invalid', - 'labels_invalid', - 'tools_unavailable', - 'busy', - 'workspace_unavailable', - 'session_unavailable', - 'history_unavailable', - 'managed_session', - 'closed', - ], - }, - }, - }, - resources: {}, - }, - selectWorkspace: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentDirectoryPath', - description: - 'A directory on the Host: an absolute path, `~` standing for the home\ndirectory, within the configured workspace locations.', - type: 'object', - properties: { - path: { - type: 'string', - minLength: 1, - }, - }, - additionalProperties: false, - required: ['path'], - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'Nullable_AgentWorkspace', - anyOf: [ - { - $ref: '#/$defs/AgentWorkspace', + minimum: 0, + }, + actual: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + }, + additionalProperties: false, + required: ['kind', 'expected', 'actual'], }, { - type: 'null', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'rejected', + }, + reason: { + $ref: '#/$defs/AgentRejection', + }, + }, + additionalProperties: false, + required: ['kind', 'reason'], }, ], $defs: { - AgentWorkspace: { + AgentSession: { type: 'object', properties: { - worktree: { + parentSessionId: { description: - 'Native owner allocation; never accepted from a caller as workspace authority.', + 'Session that initiated creation, independent of fork ancestry and task ownership.', + type: ['string', 'null'], + }, + branch: { anyOf: [ { - $ref: '#/$defs/Binding', + $ref: '#/$defs/AgentSessionBranch', }, { type: 'null', }, ], }, - projectId: { - description: "Optional membership in this Agent's canonical Project catalog.", - type: ['string', 'null'], - format: 'uuid', + backend: { + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', + }, + { + type: 'null', + }, + ], }, - path: { - type: 'string', - minLength: 1, + tools: { + type: ['array', 'null'], + items: { + type: 'string', + }, }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + dataSource: { + $ref: '#/$defs/AgentDataSource', }, - identity: { - type: 'string', - pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', + permissions: { + $ref: '#/$defs/AgentWorkspacePermissions', + }, + boundaryRevision: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + binding: { + $ref: '#/$defs/BindingStamp', }, - }, - additionalProperties: false, - required: ['path', 'directoryIdentity', 'identity'], - }, - Binding: { - description: 'Immutable allocation intent, not the current child branch or HEAD.', - type: 'object', - properties: { id: { type: 'string', }, - common_dir: { + title: { type: 'string', }, - source: { - type: 'string', + labels: { + type: 'array', + items: { + type: 'string', + }, }, - source_identity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', + workspace: { + $ref: '#/$defs/AgentWorkspace', }, - base_commit: { - type: 'string', + revision: { + type: 'integer', + format: 'uint64', + minimum: 0, }, - directory: { - type: 'string', + updatedAt: { + type: 'integer', + format: 'uint64', + minimum: 0, + }, + archived: { + type: 'boolean', + }, + pinned: { + type: 'boolean', + }, + hasUnread: { + type: 'boolean', + }, + tailMessageId: { + description: + 'Exact durable visible tail accepted by markRead; independent of preview clocks.', + type: ['string', 'null'], + }, + activity: { + $ref: '#/$defs/AgentSessionActivity', }, }, additionalProperties: false, required: [ + 'parentSessionId', + 'branch', + 'dataSource', + 'permissions', + 'boundaryRevision', + 'binding', 'id', - 'common_dir', - 'source', - 'source_identity', - 'base_commit', - 'directory', + 'title', + 'labels', + 'workspace', + 'revision', + 'updatedAt', + 'archived', + 'pinned', + 'hasUnread', + 'tailMessageId', + 'activity', ], }, - }, - }, - resources: {}, - }, - send: { - mode: 'mutation', - input: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSend', - type: 'object', - properties: { - delivery: { - $ref: '#/$defs/AgentSendDelivery', - }, - expectedInvocationId: { - description: - 'An active send must not start new work after Stop fences its original Run.', - type: ['string', 'null'], - }, - attachments: { + AgentSessionBranch: { description: - 'IDs of immutable user uploads in this Session; metadata is resolved by the owner.', - type: 'array', - items: { - type: 'string', + 'Frozen conversation ancestry for presentation, not authority over the source.', + type: 'object', + properties: { + sessionId: { + type: 'string', + minLength: 1, + }, + position: { + $ref: '#/$defs/AgentForkPosition', + }, }, - default: [], + additionalProperties: false, + required: ['sessionId', 'position'], }, - backend: { - description: 'Omission uses the session’s saved next-turn preference.', - anyOf: [ + AgentForkPosition: { + description: 'A complete logical Turn boundary within the source session.', + oneOf: [ { - $ref: '#/$defs/AgentBackendPreference', + description: 'Capture an empty history prefix.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'start', + }, + }, + required: ['kind'], + additionalProperties: false, }, { - type: 'null', + description: 'Capture all current history, including later compaction facts.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'end', + }, + }, + required: ['kind'], + additionalProperties: false, }, - ], - }, - sessionId: { - type: 'string', - minLength: 1, - }, - message: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['sessionId', 'message'], - $defs: { - AgentSendDelivery: { - oneOf: [ { - type: 'string', - enum: ['queue'], + description: 'Include the complete target Turn.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'through_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, { description: - 'Steer an active model, Codex or Claude turn; agy interrupts and resumes where supported. Other executors queue.', - type: 'string', - const: 'steer', + 'Exclude the complete target Turn, including its submitted inputs.', + type: 'object', + properties: { + kind: { + type: 'string', + const: 'before_turn', + }, + turnId: { + type: 'string', + minLength: 1, + }, + }, + additionalProperties: false, + required: ['kind', 'turnId'], }, ], }, @@ -24151,517 +23643,485 @@ export const contracts = { description: "Runtime-native effort, not a Host model's closed reasoning enum.", type: 'string', }, - }, - }, - output: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSendResult', - oneOf: [ - { + AgentDataSource: { + description: + 'Stable business-data origin, independent of package or activation revisions.', type: 'object', properties: { - kind: { + stateDomain: { type: 'string', - const: 'rejected', }, - sessionId: { + deployment: { type: 'string', + format: 'uuid', }, - reason: { - $ref: '#/$defs/AgentSendRejection', - }, - skills: { - anyOf: [ - { - $ref: '#/$defs/SkillInvocationResult', - }, - { - type: 'null', - }, - ], + namespace: { + $ref: '#/$defs/InstanceId', }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'reason'], + required: ['stateDomain', 'deployment', 'namespace'], }, - { + InstanceId: { + description: + 'Hierarchical, stable source keys. Display aliases never become data identities.', + type: 'array', + items: { + type: 'string', + format: 'uuid', + }, + }, + AgentWorkspacePermissions: { type: 'object', properties: { - kind: { - type: 'string', - const: 'started', - }, - sessionId: { - type: 'string', - }, - invocationId: { - type: 'string', + access: { + $ref: '#/$defs/AgentWorkspaceAccess', }, - messageId: { - type: 'string', + requestPermissions: { + type: 'boolean', }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'invocationId', 'messageId'], + required: ['access', 'requestPermissions'], }, - { + AgentWorkspaceAccess: { + description: + 'Session sandbox policy. Full access disables isolation and permits Computer Use\nwithout additional application, browser or audio consent.', + type: 'string', + enum: ['read_only', 'workspace_write', 'danger_full_access'], + }, + BindingStamp: { type: 'object', properties: { - kind: { - type: 'string', - const: 'queued', - }, - sessionId: { - type: 'string', - }, - messageId: { + id: { type: 'string', + format: 'uuid', }, - revision: { + permissionEpoch: { type: 'integer', format: 'uint64', minimum: 0, }, }, additionalProperties: false, - required: ['kind', 'sessionId', 'messageId', 'revision'], + required: ['id', 'permissionEpoch'], }, - { - description: - 'The caller may not send to this conversation: the Bot sends only to the\nworkers it created. Nothing was admitted.', + AgentWorkspace: { type: 'object', properties: { - kind: { - type: 'string', - const: 'not_your_worker', + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], }, - sessionId: { - type: 'string', + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', }, - }, - additionalProperties: false, - required: ['kind', 'sessionId'], - }, - ], - $defs: { - AgentSendRejection: { - type: 'string', - enum: ['model_unavailable', 'skills_unavailable', 'turn_stopped'], - }, - SkillInvocationResult: { - type: 'object', - properties: { - loaded: { - type: 'array', - items: { - $ref: '#/$defs/LoadedSkill', - }, + path: { + type: 'string', + minLength: 1, }, - failed: { - type: 'array', - items: { - $ref: '#/$defs/SkillInvocationFailure', - }, + directoryIdentity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - receipts: { - type: 'array', - items: { - $ref: '#/$defs/SkillInvocationReceipt', - }, + identity: { + type: 'string', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: ['loaded', 'failed', 'receipts'], + required: ['path', 'directoryIdentity', 'identity'], }, - LoadedSkill: { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { id: { type: 'string', }, - name: { + common_dir: { type: 'string', }, - }, - additionalProperties: false, - required: ['id', 'name'], - }, - SkillInvocationFailure: { - anyOf: [ - { - $ref: '#/$defs/SkillRequestFailure', + source: { + type: 'string', }, - ], - }, - SkillRequestFailure: { - type: 'object', - properties: { - request: { + source_identity: { type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', }, - reason: { - $ref: '#/$defs/SkillFailureReason', + base_commit: { + type: 'string', + }, + directory: { + type: 'string', }, }, additionalProperties: false, - required: ['request', 'reason'], - }, - SkillFailureReason: { - type: 'string', - enum: [ - 'invalid_name', - 'not_found', - 'disabled', - 'host_incompatible', - 'resolution_failed', - ], - }, - SkillInvocationReceipt: { - description: - 'Boolean wire discriminants are validated here, not retained as mutable\nbusiness fields beside a contradictory enum variant.', - anyOf: [ - { - type: 'object', - properties: { - invocation: { - $ref: '#/$defs/SkillInvocationMode', - }, - request: { - type: 'string', - }, - success: { - const: true, - }, - ref: { - type: 'string', - }, - id: { - type: 'string', - }, - name: { - type: 'string', - }, - scope: { - $ref: '#/$defs/SkillScope', - }, - source: { - $ref: '#/$defs/SkillSource', - }, - }, - additionalProperties: false, - required: [ - 'invocation', - 'request', - 'success', - 'ref', - 'id', - 'name', - 'scope', - 'source', - ], - }, - { - type: 'object', - properties: { - invocation: { - $ref: '#/$defs/SkillInvocationMode', - }, - request: { - type: 'string', - }, - success: { - const: false, - }, - reason: { - $ref: '#/$defs/SkillFailureReason', - }, - }, - additionalProperties: false, - required: ['invocation', 'request', 'success', 'reason'], - }, + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', ], }, - SkillInvocationMode: { - type: 'string', - enum: ['explicit', 'model_tool'], - }, - SkillScope: { + AgentSessionActivity: { type: 'string', - enum: ['project', 'workspace', 'user', 'custom'], + enum: ['idle', 'working', 'waiting'], }, - SkillSource: { + AgentRejection: { type: 'string', - enum: ['maka', 'agents', 'custom'], + enum: [ + 'permission_change_requires_user', + 'title_invalid', + 'labels_invalid', + 'tools_unavailable', + 'busy', + 'workspace_unavailable', + 'session_unavailable', + 'history_unavailable', + 'managed_session', + 'closed', + ], }, }, }, resources: {}, }, - sessionCheckpointReceipt: { - mode: 'query', + selectWorkspace: { + mode: 'mutation', input: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionCheckpointReceiptRead', + title: 'AgentDirectoryPath', + description: + 'A directory on the Host: an absolute path, `~` standing for the home\ndirectory, within the configured workspace locations.', type: 'object', properties: { - reference: { - $ref: '#/$defs/RecoveryRef', - }, - input: { - $ref: '#/$defs/AgentSessionCheckpointPublish', + path: { + type: 'string', + minLength: 1, }, }, additionalProperties: false, - required: ['reference', 'input'], + required: ['path'], + }, + output: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Nullable_AgentWorkspace', + anyOf: [ + { + $ref: '#/$defs/AgentWorkspace', + }, + { + type: 'null', + }, + ], $defs: { - RecoveryRef: { + AgentWorkspace: { type: 'object', properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', + worktree: { + description: + 'Native owner allocation; never accepted from a caller as workspace authority.', + anyOf: [ + { + $ref: '#/$defs/Binding', + }, + { + type: 'null', + }, + ], }, - export: { - type: 'string', + projectId: { + description: "Optional membership in this Agent's canonical Project catalog.", + type: ['string', 'null'], + format: 'uuid', }, - method: { + path: { type: 'string', + minLength: 1, }, - operation: { + directoryIdentity: { type: 'string', - format: 'uuid', + pattern: '^sha256:[a-f0-9]{64}$', }, - fingerprint: { + identity: { type: 'string', - }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', + pattern: '^workspace:v1:[A-Fa-f0-9-]{36}$', }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['path', 'directoryIdentity', 'identity'], }, - DeploymentRef: { + Binding: { + description: 'Immutable allocation intent, not the current child branch or HEAD.', type: 'object', properties: { - stateDomain: { + id: { type: 'string', }, - deployment: { + common_dir: { type: 'string', - format: 'uuid', }, - revision: { - type: 'integer', - format: 'uint64', - minimum: 0, + source: { + type: 'string', + }, + source_identity: { + type: 'string', + pattern: '^sha256:[a-f0-9]{64}$', + }, + base_commit: { + type: 'string', + }, + directory: { + type: 'string', }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], + required: [ + 'id', + 'common_dir', + 'source', + 'source_identity', + 'base_commit', + 'directory', + ], }, - InstanceId: { + }, + }, + resources: {}, + }, + send: { + mode: 'mutation', + input: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'AgentSend', + type: 'object', + properties: { + delivery: { + $ref: '#/$defs/AgentSendDelivery', + }, + expectedInvocationId: { description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', + 'An active send must not start new work after Stop fences its original Run.', + type: ['string', 'null'], + }, + attachments: { + description: + 'IDs of immutable user uploads in this Session; metadata is resolved by the owner.', type: 'array', items: { type: 'string', - format: 'uuid', }, + default: [], }, - RecoveryContract: { - description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', - type: 'object', - properties: { - name: { - type: 'string', + backend: { + description: 'Omission uses the session’s saved next-turn preference.', + anyOf: [ + { + $ref: '#/$defs/AgentBackendPreference', }, - version: { - type: 'integer', - format: 'uint32', - minimum: 0, + { + type: 'null', }, - transport: { - $ref: '#/$defs/Transport', + ], + }, + sessionId: { + type: 'string', + minLength: 1, + }, + message: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['sessionId', 'message'], + $defs: { + AgentSendDelivery: { + oneOf: [ + { + type: 'string', + enum: ['queue'], }, - digest: { + { + description: + 'Steer an active model, Codex or Claude turn; agy interrupts and resumes where supported. Other executors queue.', type: 'string', + const: 'steer', }, - }, - additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], + ], }, - Transport: { - type: 'string', - enum: ['local', 'remote'], + AgentBackendPreference: { + description: + "The next turn's choice, independent of an already admitted execution.\nModel revisions are resolved afresh; preferences never grant execution authority.", + oneOf: [ + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'model', + }, + connectionId: { + type: 'string', + minLength: 1, + }, + modelId: { + type: 'string', + minLength: 1, + }, + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ThinkingLevel', + }, + { + type: 'null', + }, + ], + }, + }, + additionalProperties: false, + required: ['kind', 'connectionId', 'modelId'], + }, + { + type: 'object', + properties: { + kind: { + type: 'string', + const: 'executor', + }, + selection: { + $ref: '#/$defs/AgentExecutorSelection', + }, + }, + additionalProperties: false, + required: ['kind', 'selection'], + }, + ], }, - RecoveryProtocol: { + ThinkingLevel: { type: 'string', - enum: ['journal_v1'], + enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'], }, - AgentSessionCheckpointPublish: { + AgentExecutorSelection: { type: 'object', properties: { - history: { - $ref: '#/$defs/AgentHistoryCheckpointPublish', + executor: { + type: 'string', + minLength: 1, }, - workspaceConsistency: { - description: - 'Must be explicitly selected. Maka does not fence other applications.', - $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + settings: { + $ref: '#/$defs/ExecutorSettings', }, }, additionalProperties: false, - required: ['history', 'workspaceConsistency'], + required: ['executor', 'settings'], }, - AgentHistoryCheckpointPublish: { + ExecutorSettings: { + description: + 'Executor-owned model selection, independent of Host model connections.', type: 'object', properties: { - sessionId: { - type: 'string', - minLength: 1, - }, - expectedRevision: { + model: { type: ['string', 'null'], - format: 'uuid', }, - confirmedSubtreeDigest: { - description: - 'Membership confirmation returned by previewHistoryCheckpoint, not a live revision.', - type: 'string', - pattern: '^[a-f0-9]{64}$', + thinkingLevel: { + anyOf: [ + { + $ref: '#/$defs/ExecutorEffort', + }, + { + type: 'null', + }, + ], + }, + runtime: { + type: ['string', 'null'], }, }, additionalProperties: false, - required: ['sessionId', 'expectedRevision', 'confirmedSubtreeDigest'], }, - AgentCheckpointWorkspaceConsistency: { - oneOf: [ - { - description: - 'The caller confirms all other writers remain paused for the entire copy.', - type: 'string', - const: 'caller_confirmed_quiescent', - }, - { - description: - 'Files may not represent one instant; successful digest checks do not change this.', - type: 'string', - const: 'unverified', - }, - ], + ExecutorEffort: { + description: "Runtime-native effort, not a Host model's closed reasoning enum.", + type: 'string', }, }, }, output: { $schema: 'https://json-schema.org/draft/2020-12/schema', - title: 'AgentSessionCheckpointReceipt', - type: 'object', - properties: { - operation: { - anyOf: [ - { - $ref: '#/$defs/OperationObservation', - }, - { - type: 'null', - }, - ], - }, - checkpoint: { - anyOf: [ - { - $ref: '#/$defs/AgentSessionCheckpointPublication', - }, - { - type: 'null', - }, - ], - }, - }, - additionalProperties: false, - $defs: { - OperationObservation: { + title: 'AgentSendResult', + oneOf: [ + { type: 'object', properties: { - reference: { - $ref: '#/$defs/RecoveryRef', + kind: { + type: 'string', + const: 'rejected', }, - outcome: { - $ref: '#/$defs/OperationOutcome', + sessionId: { + type: 'string', }, - cleanup: { - $ref: '#/$defs/OperationCleanup', + reason: { + $ref: '#/$defs/AgentSendRejection', + }, + skills: { + anyOf: [ + { + $ref: '#/$defs/SkillInvocationResult', + }, + { + type: 'null', + }, + ], }, }, additionalProperties: false, - required: ['reference', 'outcome', 'cleanup'], + required: ['kind', 'sessionId', 'reason'], }, - RecoveryRef: { + { type: 'object', properties: { - source: { - $ref: '#/$defs/DeploymentRef', - }, - namespace: { - $ref: '#/$defs/InstanceId', - }, - export: { + kind: { type: 'string', + const: 'started', }, - method: { + sessionId: { type: 'string', }, - operation: { + invocationId: { type: 'string', - format: 'uuid', }, - fingerprint: { + messageId: { type: 'string', }, - contract: { - $ref: '#/$defs/RecoveryContract', - }, - recovery: { - $ref: '#/$defs/RecoveryProtocol', - }, }, additionalProperties: false, - required: [ - 'source', - 'namespace', - 'export', - 'method', - 'operation', - 'fingerprint', - 'contract', - 'recovery', - ], + required: ['kind', 'sessionId', 'invocationId', 'messageId'], }, - DeploymentRef: { + { type: 'object', properties: { - stateDomain: { + kind: { type: 'string', + const: 'queued', }, - deployment: { + sessionId: { + type: 'string', + }, + messageId: { type: 'string', - format: 'uuid', }, revision: { type: 'integer', @@ -24670,295 +24130,174 @@ export const contracts = { }, }, additionalProperties: false, - required: ['stateDomain', 'deployment', 'revision'], - }, - InstanceId: { - description: - 'Hierarchical, stable source keys. Display aliases never become data identities.', - type: 'array', - items: { - type: 'string', - format: 'uuid', - }, + required: ['kind', 'sessionId', 'messageId', 'revision'], }, - RecoveryContract: { + { description: - 'Contract identity for a journal lookup. The original request retains the\ncomplete contract; a receipt carries only its digest.', + 'The caller may not send to this conversation: the Bot sends only to the\nworkers it created. Nothing was admitted.', type: 'object', properties: { - name: { + kind: { type: 'string', + const: 'not_your_worker', }, - version: { - type: 'integer', - format: 'uint32', - minimum: 0, - }, - transport: { - $ref: '#/$defs/Transport', - }, - digest: { + sessionId: { type: 'string', }, }, additionalProperties: false, - required: ['name', 'version', 'transport', 'digest'], - }, - Transport: { - type: 'string', - enum: ['local', 'remote'], + required: ['kind', 'sessionId'], }, - RecoveryProtocol: { + ], + $defs: { + AgentSendRejection: { type: 'string', - enum: ['journal_v1'], - }, - OperationOutcome: { - description: - 'Unresolved means no durable result is known, including after process loss.', - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unresolved', - }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'returned', - }, - value: true, - }, - additionalProperties: false, - required: ['kind', 'value'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unknown', - }, - diagnostic: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['kind', 'diagnostic'], - }, - ], - }, - OperationCleanup: { - oneOf: [ - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'unconfirmed', - }, + enum: ['model_unavailable', 'skills_unavailable', 'turn_stopped'], + }, + SkillInvocationResult: { + type: 'object', + properties: { + loaded: { + type: 'array', + items: { + $ref: '#/$defs/LoadedSkill', }, - required: ['kind'], - additionalProperties: false, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'confirmed', - }, + failed: { + type: 'array', + items: { + $ref: '#/$defs/SkillInvocationFailure', }, - required: ['kind'], - additionalProperties: false, }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'failed', - }, - diagnostic: { - type: 'string', - }, + receipts: { + type: 'array', + items: { + $ref: '#/$defs/SkillInvocationReceipt', }, - additionalProperties: false, - required: ['kind', 'diagnostic'], }, - ], + }, + additionalProperties: false, + required: ['loaded', 'failed', 'receipts'], }, - AgentSessionCheckpointPublication: { + LoadedSkill: { type: 'object', properties: { - publication: { - $ref: '#/$defs/AgentHistoryCheckpointPublication', - }, - workspaceConsistency: { - $ref: '#/$defs/AgentCheckpointWorkspaceConsistency', + id: { + type: 'string', }, - warning: { - description: 'Stable warning, present even with a caller attestation.', - $ref: '#/$defs/AgentCheckpointWorkspaceWarning', + name: { + type: 'string', }, }, additionalProperties: false, - required: ['publication', 'workspaceConsistency', 'warning'], + required: ['id', 'name'], + }, + SkillInvocationFailure: { + anyOf: [ + { + $ref: '#/$defs/SkillRequestFailure', + }, + ], }, - AgentHistoryCheckpointPublication: { + SkillRequestFailure: { type: 'object', properties: { - state: { - $ref: '#/$defs/AgentHistoryCheckpointState', + request: { + type: 'string', }, - cleanupPending: { - description: 'Cleanup failure does not undo a successful publication.', - type: 'boolean', + reason: { + $ref: '#/$defs/SkillFailureReason', }, }, additionalProperties: false, - required: ['state', 'cleanupPending'], + required: ['request', 'reason'], }, - AgentHistoryCheckpointState: { - oneOf: [ + SkillFailureReason: { + type: 'string', + enum: [ + 'invalid_name', + 'not_found', + 'disabled', + 'host_incompatible', + 'resolution_failed', + ], + }, + SkillInvocationReceipt: { + description: + 'Boolean wire discriminants are validated here, not retained as mutable\nbusiness fields beside a contradictory enum variant.', + anyOf: [ { type: 'object', properties: { - kind: { - type: 'string', - const: 'capturing', + invocation: { + $ref: '#/$defs/SkillInvocationMode', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { + request: { type: 'string', - const: 'prepared', }, - }, - required: ['kind'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { + success: { + const: true, + }, + ref: { type: 'string', - const: 'published', }, - head: { - $ref: '#/$defs/AgentHistoryCheckpointHead', + id: { + type: 'string', }, - }, - additionalProperties: false, - required: ['kind', 'head'], - }, - { - type: 'object', - properties: { - kind: { + name: { type: 'string', - const: 'conflict', }, - actual: { - type: ['string', 'null'], - format: 'uuid', + scope: { + $ref: '#/$defs/SkillScope', }, - }, - additionalProperties: false, - required: ['kind'], - }, - { - type: 'object', - properties: { - kind: { - type: 'string', - const: 'interrupted', + source: { + $ref: '#/$defs/SkillSource', }, }, - required: ['kind'], additionalProperties: false, + required: [ + 'invocation', + 'request', + 'success', + 'ref', + 'id', + 'name', + 'scope', + 'source', + ], }, { type: 'object', properties: { - kind: { + invocation: { + $ref: '#/$defs/SkillInvocationMode', + }, + request: { type: 'string', - const: 'unavailable', + }, + success: { + const: false, + }, + reason: { + $ref: '#/$defs/SkillFailureReason', }, }, - required: ['kind'], additionalProperties: false, + required: ['invocation', 'request', 'success', 'reason'], }, ], }, - AgentHistoryCheckpointHead: { - type: 'object', - properties: { - revision: { - type: 'string', - format: 'uuid', - }, - manifest: { - $ref: '#/$defs/AgentHistoryCheckpointObject', - }, - }, - additionalProperties: false, - required: ['revision', 'manifest'], - }, - AgentHistoryCheckpointObject: { - type: 'object', - properties: { - digest: { - type: 'string', - pattern: '^[a-f0-9]{64}$', - }, - bytes: { - type: 'integer', - format: 'uint64', - minimum: 0, - maximum: 9007199254740991, - }, - }, - additionalProperties: false, - required: ['digest', 'bytes'], + SkillInvocationMode: { + type: 'string', + enum: ['explicit', 'model_tool'], }, - AgentCheckpointWorkspaceConsistency: { - oneOf: [ - { - description: - 'The caller confirms all other writers remain paused for the entire copy.', - type: 'string', - const: 'caller_confirmed_quiescent', - }, - { - description: - 'Files may not represent one instant; successful digest checks do not change this.', - type: 'string', - const: 'unverified', - }, - ], + SkillScope: { + type: 'string', + enum: ['project', 'workspace', 'user', 'custom'], }, - AgentCheckpointWorkspaceWarning: { - oneOf: [ - { - description: - 'Display to users: pause editors, terminals and other writers for the entire\ncapture. Hash verification is not a point-in-time consistency guarantee.', - type: 'string', - const: 'external_writers_not_fenced', - }, - ], + SkillSource: { + type: 'string', + enum: ['maka', 'agents', 'custom'], }, }, }, @@ -25090,7 +24429,10 @@ export const contracts = { $ref: '#/$defs/AgentHistoryCheckpointRead', }, parent: { - $ref: '#/$defs/AgentDirectorySelection', + description: + 'Absolute parent path. The Host captures and validates its directory identity.', + type: 'string', + minLength: 1, }, permissions: { $ref: '#/$defs/AgentWorkspacePermissions', @@ -25128,27 +24470,6 @@ export const contracts = { additionalProperties: false, required: ['sessionId', 'revision'], }, - AgentDirectorySelection: { - type: 'object', - properties: { - rootId: { - type: 'string', - minLength: 1, - }, - segments: { - type: 'array', - items: { - type: 'string', - }, - }, - directoryIdentity: { - type: 'string', - pattern: '^sha256:[a-f0-9]{64}$', - }, - }, - additionalProperties: false, - required: ['rootId', 'segments', 'directoryIdentity'], - }, AgentWorkspacePermissions: { type: 'object', properties: { diff --git a/packages/plugin-sdk/src/generated/methods.ts b/packages/plugin-sdk/src/generated/methods.ts index 329e4b12ca5..2c8021afbe8 100644 --- a/packages/plugin-sdk/src/generated/methods.ts +++ b/packages/plugin-sdk/src/generated/methods.ts @@ -20,36 +20,25 @@ // Generated from Rust runtime schemas. Run npm run schema:generate -w @maka-agent/plugin-sdk. import type * as Runtime from './runtime.js'; -export type AgentCompactOutput_AgentExecutionState = - | 'admitted' - | 'running' - | 'waiting' - | 'paused' - | 'completed' - | 'failed' - | 'cancelled'; -export type AgentCompactOutput_AgentCompactRejection = - | 'busy' - | 'closed' - | 'archived' - | 'backend_unavailable' - | 'workspace_changed'; /** * Hierarchical, stable source keys. Display aliases never become data identities. */ -export type AgentHistoryCheckpointLoadReceiptInput_InstanceId = string[]; -export type AgentHistoryCheckpointLoadReceiptInput_Transport = 'local' | 'remote'; -export type AgentHistoryCheckpointLoadReceiptInput_RecoveryProtocol = 'journal_v1'; +export type AgentCheckpointReceiptInput_InstanceId = string[]; +export type AgentCheckpointReceiptInput_Transport = 'local' | 'remote'; +export type AgentCheckpointReceiptInput_RecoveryProtocol = 'journal_v1'; +export type AgentCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; /** * Hierarchical, stable source keys. Display aliases never become data identities. */ -export type AgentHistoryCheckpointLoadReceiptOutput_InstanceId = string[]; -export type AgentHistoryCheckpointLoadReceiptOutput_Transport = 'local' | 'remote'; -export type AgentHistoryCheckpointLoadReceiptOutput_RecoveryProtocol = 'journal_v1'; +export type AgentCheckpointReceiptOutput_InstanceId = string[]; +export type AgentCheckpointReceiptOutput_Transport = 'local' | 'remote'; +export type AgentCheckpointReceiptOutput_RecoveryProtocol = 'journal_v1'; /** * Unresolved means no durable result is known, including after process loss. */ -export type AgentHistoryCheckpointLoadReceiptOutput_OperationOutcome = +export type AgentCheckpointReceiptOutput_OperationOutcome = | { kind: 'unresolved'; } @@ -61,7 +50,7 @@ export type AgentHistoryCheckpointLoadReceiptOutput_OperationOutcome = kind: 'unknown'; diagnostic: string; }; -export type AgentHistoryCheckpointLoadReceiptOutput_OperationCleanup = +export type AgentCheckpointReceiptOutput_OperationCleanup = | { kind: 'unconfirmed'; } @@ -72,39 +61,62 @@ export type AgentHistoryCheckpointLoadReceiptOutput_OperationCleanup = kind: 'failed'; diagnostic: string; }; -export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoadState = +export type AgentCheckpointReceiptOutput_AgentHistoryCheckpointState = | { - kind: 'preparing'; + kind: 'capturing'; } | { - kind: 'ready'; - sessionIds: string[]; - workspace?: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceSnapshot | null; + kind: 'prepared'; + } + | { + kind: 'published'; + head: AgentCheckpointReceiptOutput_AgentHistoryCheckpointHead; + } + | { + kind: 'conflict'; + actual?: string | null; + } + | { + kind: 'interrupted'; } | { kind: 'unavailable'; }; -export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceConsistency = +export type AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency = | 'caller_confirmed_quiescent' | 'unverified'; -export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceWarning = +export type AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning = 'external_writers_not_fenced'; +export type AgentCompactOutput_AgentExecutionState = + | 'admitted' + | 'running' + | 'waiting' + | 'paused' + | 'completed' + | 'failed' + | 'cancelled'; +export type AgentCompactOutput_AgentCompactRejection = + | 'busy' + | 'closed' + | 'archived' + | 'backend_unavailable' + | 'workspace_changed'; /** * Hierarchical, stable source keys. Display aliases never become data identities. */ -export type AgentHistoryCheckpointReceiptInput_InstanceId = string[]; -export type AgentHistoryCheckpointReceiptInput_Transport = 'local' | 'remote'; -export type AgentHistoryCheckpointReceiptInput_RecoveryProtocol = 'journal_v1'; +export type AgentHistoryCheckpointLoadReceiptInput_InstanceId = string[]; +export type AgentHistoryCheckpointLoadReceiptInput_Transport = 'local' | 'remote'; +export type AgentHistoryCheckpointLoadReceiptInput_RecoveryProtocol = 'journal_v1'; /** * Hierarchical, stable source keys. Display aliases never become data identities. */ -export type AgentHistoryCheckpointReceiptOutput_InstanceId = string[]; -export type AgentHistoryCheckpointReceiptOutput_Transport = 'local' | 'remote'; -export type AgentHistoryCheckpointReceiptOutput_RecoveryProtocol = 'journal_v1'; +export type AgentHistoryCheckpointLoadReceiptOutput_InstanceId = string[]; +export type AgentHistoryCheckpointLoadReceiptOutput_Transport = 'local' | 'remote'; +export type AgentHistoryCheckpointLoadReceiptOutput_RecoveryProtocol = 'journal_v1'; /** * Unresolved means no durable result is known, including after process loss. */ -export type AgentHistoryCheckpointReceiptOutput_OperationOutcome = +export type AgentHistoryCheckpointLoadReceiptOutput_OperationOutcome = | { kind: 'unresolved'; } @@ -116,7 +128,7 @@ export type AgentHistoryCheckpointReceiptOutput_OperationOutcome = kind: 'unknown'; diagnostic: string; }; -export type AgentHistoryCheckpointReceiptOutput_OperationCleanup = +export type AgentHistoryCheckpointLoadReceiptOutput_OperationCleanup = | { kind: 'unconfirmed'; } @@ -127,27 +139,23 @@ export type AgentHistoryCheckpointReceiptOutput_OperationCleanup = kind: 'failed'; diagnostic: string; }; -export type AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointState = - | { - kind: 'capturing'; - } - | { - kind: 'prepared'; - } - | { - kind: 'published'; - head: AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointHead; - } +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoadState = | { - kind: 'conflict'; - actual?: string | null; + kind: 'preparing'; } | { - kind: 'interrupted'; + kind: 'ready'; + sessionIds: string[]; + workspace?: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceSnapshot | null; } | { kind: 'unavailable'; }; +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceConsistency = + | 'caller_confirmed_quiescent' + | 'unverified'; +export type AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceWarning = + 'external_writers_not_fenced'; export type AgentLoadHistoryCheckpointOutput_AgentCheckpointLoadState = | { kind: 'preparing'; @@ -203,31 +211,10 @@ export type AgentOpenBotOutput_AgentBotRejection = | 'creation_rejected' | 'no_workspace'; export type AgentPredictNextMessageInput_AgentPredictionTrigger = 'live' | 'catch_up'; -export type AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointState = - | { - kind: 'capturing'; - } - | { - kind: 'prepared'; - } - | { - kind: 'published'; - head: AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointHead; - } - | { - kind: 'conflict'; - actual?: string | null; - } - | { - kind: 'interrupted'; - } - | { - kind: 'unavailable'; - }; -export type AgentPublishSessionCheckpointInput_AgentCheckpointWorkspaceConsistency = +export type AgentPublishCheckpointInput_AgentCheckpointWorkspaceConsistency = | 'caller_confirmed_quiescent' | 'unverified'; -export type AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState = +export type AgentPublishCheckpointOutput_AgentHistoryCheckpointState = | { kind: 'capturing'; } @@ -236,7 +223,7 @@ export type AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState = } | { kind: 'published'; - head: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointHead; + head: AgentPublishCheckpointOutput_AgentHistoryCheckpointHead; } | { kind: 'conflict'; @@ -248,10 +235,10 @@ export type AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState = | { kind: 'unavailable'; }; -export type AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency = +export type AgentPublishCheckpointOutput_AgentCheckpointWorkspaceConsistency = | 'caller_confirmed_quiescent' | 'unverified'; -export type AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceWarning = +export type AgentPublishCheckpointOutput_AgentCheckpointWorkspaceWarning = 'external_writers_not_fenced'; /** * All parts are subscription items; only End admits a complete preview. @@ -301,17 +288,11 @@ export type AgentReadFileOutput_AgentFileContent = export type AgentReadFileOutput_AgentFileKind = 'file' | 'directory' | 'symlink' | 'other'; export type AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointFrame = | { - kind: 'native_session_v1'; - head: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead; - inventory: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointInventory; - bundle: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointObject; - workspace: AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot; - } - | { - kind: 'native_history_v1'; + kind: 'checkpoint'; head: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead; inventory: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointInventory; bundle: AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointObject; + workspace?: AgentReadHistoryCheckpointOutput_AgentCheckpointWorkspaceSnapshot | null; } | { kind: 'chunk'; @@ -372,73 +353,6 @@ export type AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceWarning /** * Hierarchical, stable source keys. Display aliases never become data identities. */ -export type AgentSessionCheckpointReceiptInput_InstanceId = string[]; -export type AgentSessionCheckpointReceiptInput_Transport = 'local' | 'remote'; -export type AgentSessionCheckpointReceiptInput_RecoveryProtocol = 'journal_v1'; -export type AgentSessionCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency = - | 'caller_confirmed_quiescent' - | 'unverified'; -/** - * Hierarchical, stable source keys. Display aliases never become data identities. - */ -export type AgentSessionCheckpointReceiptOutput_InstanceId = string[]; -export type AgentSessionCheckpointReceiptOutput_Transport = 'local' | 'remote'; -export type AgentSessionCheckpointReceiptOutput_RecoveryProtocol = 'journal_v1'; -/** - * Unresolved means no durable result is known, including after process loss. - */ -export type AgentSessionCheckpointReceiptOutput_OperationOutcome = - | { - kind: 'unresolved'; - } - | { - kind: 'returned'; - value: unknown; - } - | { - kind: 'unknown'; - diagnostic: string; - }; -export type AgentSessionCheckpointReceiptOutput_OperationCleanup = - | { - kind: 'unconfirmed'; - } - | { - kind: 'confirmed'; - } - | { - kind: 'failed'; - diagnostic: string; - }; -export type AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointState = - | { - kind: 'capturing'; - } - | { - kind: 'prepared'; - } - | { - kind: 'published'; - head: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointHead; - } - | { - kind: 'conflict'; - actual?: string | null; - } - | { - kind: 'interrupted'; - } - | { - kind: 'unavailable'; - }; -export type AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency = - | 'caller_confirmed_quiescent' - | 'unverified'; -export type AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning = - 'external_writers_not_fenced'; -/** - * Hierarchical, stable source keys. Display aliases never become data identities. - */ export type AgentSessionCheckpointRestoreReceiptInput_InstanceId = string[]; export type AgentSessionCheckpointRestoreReceiptInput_Transport = 'local' | 'remote'; export type AgentSessionCheckpointRestoreReceiptInput_RecoveryProtocol = 'journal_v1'; @@ -786,6 +700,27 @@ export interface ServiceMethods { input: Runtime.AgentChecklistRead; output: Runtime.AgentChecklist; }; + checkpointReceipt: { + mode: 'query'; + input: { + reference: AgentCheckpointReceiptInput_RecoveryRef; + /** + * The exact original input. Changed input cannot select another publication. + */ + input: AgentCheckpointReceiptInput_AgentCheckpointPublish; + }; + output: { + /** + * Original Host observation. Domain success never fabricates Host cleanup confirmation. + */ + operation?: AgentCheckpointReceiptOutput_OperationObservation | null; + /** + * Authoritative domain result even when the Host response was lost. + * Absence is not permission to recapture under a replacement operation. + */ + publication?: AgentCheckpointReceiptOutput_AgentCheckpointPublication | null; + }; + }; compact: { mode: 'mutation'; input: { @@ -933,27 +868,6 @@ export interface ServiceMethods { load?: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointLoad | null; }; }; - historyCheckpointReceipt: { - mode: 'query'; - input: { - reference: AgentHistoryCheckpointReceiptInput_RecoveryRef; - /** - * The exact original input. Changed input cannot select another publication. - */ - input: AgentHistoryCheckpointReceiptInput_AgentHistoryCheckpointPublish; - }; - output: { - /** - * Original Host observation. Domain success never fabricates Host cleanup confirmation. - */ - operation?: AgentHistoryCheckpointReceiptOutput_OperationObservation | null; - /** - * Authoritative domain result even when the Host response was lost. - * Absence is not permission to recapture under a replacement operation. - */ - publication?: AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointPublication | null; - }; - }; historyLandmarks: { mode: 'query'; input: Runtime.AgentHistoryLandmarksRead; @@ -1010,7 +924,7 @@ export interface ServiceMethods { }; }; loadedHistoryCheckpointArtifact: { - mode: 'query'; + mode: 'subscription'; input: { inspection: AgentLoadedHistoryCheckpointArtifactInput_AgentCheckpointInspection; sessionId: string; @@ -1155,7 +1069,7 @@ export interface ServiceMethods { input: Runtime.AgentProjectsRead; output: Runtime.AgentProjectsPage; }; - publishHistoryCheckpoint: { + publishCheckpoint: { mode: 'mutation'; input: { sessionId: string; @@ -1164,31 +1078,19 @@ export interface ServiceMethods { * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. */ confirmedSubtreeDigest: string; - }; - output: { - state: AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointState; - /** - * Cleanup failure does not undo a successful publication. - */ - cleanupPending: boolean; - }; - }; - publishSessionCheckpoint: { - mode: 'mutation'; - input: { - history: AgentPublishSessionCheckpointInput_AgentHistoryCheckpointPublish; /** - * Must be explicitly selected. Maka does not fence other applications. + * Null captures history only. Otherwise the caller explicitly selects the + * Workspace consistency guarantee; other applications are never fenced. */ - workspaceConsistency: AgentPublishSessionCheckpointInput_AgentCheckpointWorkspaceConsistency; + workspaceConsistency: AgentPublishCheckpointInput_AgentCheckpointWorkspaceConsistency | null; }; output: { - publication: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointPublication; - workspaceConsistency: AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceConsistency; + state: AgentPublishCheckpointOutput_AgentHistoryCheckpointState; /** - * Stable warning, present even with a caller attestation. + * Cleanup failure does not undo a successful publication. */ - warning: AgentPublishSessionCheckpointOutput_AgentCheckpointWorkspaceWarning; + cleanupPending: boolean; + workspace?: AgentPublishCheckpointOutput_AgentCheckpointWorkspaceNotice | null; }; }; queue: { @@ -1265,7 +1167,10 @@ export interface ServiceMethods { */ input: { checkpoint: AgentRestoreSessionCheckpointInput_AgentHistoryCheckpointRead; - parent: AgentRestoreSessionCheckpointInput_AgentDirectorySelection; + /** + * Absolute parent path. The Host captures and validates its directory identity. + */ + parent: string; permissions: AgentRestoreSessionCheckpointInput_AgentWorkspacePermissions; backend: AgentRestoreSessionCheckpointInput_AgentBackendPreference; acceptExternalWritersRisk: boolean; @@ -1330,17 +1235,6 @@ export interface ServiceMethods { input: Runtime.AgentSend; output: Runtime.AgentSendResult; }; - sessionCheckpointReceipt: { - mode: 'query'; - input: { - reference: AgentSessionCheckpointReceiptInput_RecoveryRef; - input: AgentSessionCheckpointReceiptInput_AgentSessionCheckpointPublish; - }; - output: { - operation?: AgentSessionCheckpointReceiptOutput_OperationObservation | null; - checkpoint?: AgentSessionCheckpointReceiptOutput_AgentSessionCheckpointPublication | null; - }; - }; sessionCheckpointRestoreReceipt: { mode: 'query'; input: { @@ -2229,6 +2123,94 @@ export interface AgentBotOutboxOutput_AgentBotMessage { */ turnId?: string | null; } +export interface AgentCheckpointReceiptInput_RecoveryRef { + source: AgentCheckpointReceiptInput_DeploymentRef; + namespace: AgentCheckpointReceiptInput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentCheckpointReceiptInput_RecoveryContract; + recovery: AgentCheckpointReceiptInput_RecoveryProtocol; +} +export interface AgentCheckpointReceiptInput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentCheckpointReceiptInput_RecoveryContract { + name: string; + version: number; + transport: AgentCheckpointReceiptInput_Transport; + digest: string; +} +export interface AgentCheckpointReceiptInput_AgentCheckpointPublish { + sessionId: string; + expectedRevision: string | null; + /** + * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. + */ + confirmedSubtreeDigest: string; + /** + * Null captures history only. Otherwise the caller explicitly selects the + * Workspace consistency guarantee; other applications are never fenced. + */ + workspaceConsistency: AgentCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency | null; +} +export interface AgentCheckpointReceiptOutput_OperationObservation { + reference: AgentCheckpointReceiptOutput_RecoveryRef; + outcome: AgentCheckpointReceiptOutput_OperationOutcome; + cleanup: AgentCheckpointReceiptOutput_OperationCleanup; +} +export interface AgentCheckpointReceiptOutput_RecoveryRef { + source: AgentCheckpointReceiptOutput_DeploymentRef; + namespace: AgentCheckpointReceiptOutput_InstanceId; + export: string; + method: string; + operation: string; + fingerprint: string; + contract: AgentCheckpointReceiptOutput_RecoveryContract; + recovery: AgentCheckpointReceiptOutput_RecoveryProtocol; +} +export interface AgentCheckpointReceiptOutput_DeploymentRef { + stateDomain: string; + deployment: string; + revision: number; +} +/** + * Contract identity for a journal lookup. The original request retains the + * complete contract; a receipt carries only its digest. + */ +export interface AgentCheckpointReceiptOutput_RecoveryContract { + name: string; + version: number; + transport: AgentCheckpointReceiptOutput_Transport; + digest: string; +} +export interface AgentCheckpointReceiptOutput_AgentCheckpointPublication { + state: AgentCheckpointReceiptOutput_AgentHistoryCheckpointState; + /** + * Cleanup failure does not undo a successful publication. + */ + cleanupPending: boolean; + workspace?: AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceNotice | null; +} +export interface AgentCheckpointReceiptOutput_AgentHistoryCheckpointHead { + revision: string; + manifest: AgentCheckpointReceiptOutput_AgentHistoryCheckpointObject; +} +export interface AgentCheckpointReceiptOutput_AgentHistoryCheckpointObject { + digest: string; + bytes: number; +} +export interface AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceNotice { + consistency: AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning; +} /** * Canonical physical execution receipt, independent of the current Session tip. */ @@ -2336,84 +2318,6 @@ export interface AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspac warning: AgentHistoryCheckpointLoadReceiptOutput_AgentCheckpointWorkspaceWarning; inventory: AgentHistoryCheckpointLoadReceiptOutput_AgentHistoryCheckpointObject; } -export interface AgentHistoryCheckpointReceiptInput_RecoveryRef { - source: AgentHistoryCheckpointReceiptInput_DeploymentRef; - namespace: AgentHistoryCheckpointReceiptInput_InstanceId; - export: string; - method: string; - operation: string; - fingerprint: string; - contract: AgentHistoryCheckpointReceiptInput_RecoveryContract; - recovery: AgentHistoryCheckpointReceiptInput_RecoveryProtocol; -} -export interface AgentHistoryCheckpointReceiptInput_DeploymentRef { - stateDomain: string; - deployment: string; - revision: number; -} -/** - * Contract identity for a journal lookup. The original request retains the - * complete contract; a receipt carries only its digest. - */ -export interface AgentHistoryCheckpointReceiptInput_RecoveryContract { - name: string; - version: number; - transport: AgentHistoryCheckpointReceiptInput_Transport; - digest: string; -} -export interface AgentHistoryCheckpointReceiptInput_AgentHistoryCheckpointPublish { - sessionId: string; - expectedRevision: string | null; - /** - * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. - */ - confirmedSubtreeDigest: string; -} -export interface AgentHistoryCheckpointReceiptOutput_OperationObservation { - reference: AgentHistoryCheckpointReceiptOutput_RecoveryRef; - outcome: AgentHistoryCheckpointReceiptOutput_OperationOutcome; - cleanup: AgentHistoryCheckpointReceiptOutput_OperationCleanup; -} -export interface AgentHistoryCheckpointReceiptOutput_RecoveryRef { - source: AgentHistoryCheckpointReceiptOutput_DeploymentRef; - namespace: AgentHistoryCheckpointReceiptOutput_InstanceId; - export: string; - method: string; - operation: string; - fingerprint: string; - contract: AgentHistoryCheckpointReceiptOutput_RecoveryContract; - recovery: AgentHistoryCheckpointReceiptOutput_RecoveryProtocol; -} -export interface AgentHistoryCheckpointReceiptOutput_DeploymentRef { - stateDomain: string; - deployment: string; - revision: number; -} -/** - * Contract identity for a journal lookup. The original request retains the - * complete contract; a receipt carries only its digest. - */ -export interface AgentHistoryCheckpointReceiptOutput_RecoveryContract { - name: string; - version: number; - transport: AgentHistoryCheckpointReceiptOutput_Transport; - digest: string; -} -export interface AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointPublication { - state: AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointState; - /** - * Cleanup failure does not undo a successful publication. - */ - cleanupPending: boolean; -} -export interface AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointHead { - revision: string; - manifest: AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointObject; -} -export interface AgentHistoryCheckpointReceiptOutput_AgentHistoryCheckpointObject { - digest: string; - bytes: number; -} export interface AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointHead { revision: string; manifest: AgentLoadHistoryCheckpointOutput_AgentHistoryCheckpointObject; @@ -2496,36 +2400,17 @@ export interface AgentProjectMcpOutput_AgentMcpServerView { preapproved: boolean; readOnlyTools: string[]; } -export interface AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointHead { +export interface AgentPublishCheckpointOutput_AgentHistoryCheckpointHead { revision: string; - manifest: AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointObject; + manifest: AgentPublishCheckpointOutput_AgentHistoryCheckpointObject; } -export interface AgentPublishHistoryCheckpointOutput_AgentHistoryCheckpointObject { +export interface AgentPublishCheckpointOutput_AgentHistoryCheckpointObject { digest: string; bytes: number; } -export interface AgentPublishSessionCheckpointInput_AgentHistoryCheckpointPublish { - sessionId: string; - expectedRevision: string | null; - /** - * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. - */ - confirmedSubtreeDigest: string; -} -export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointPublication { - state: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointState; - /** - * Cleanup failure does not undo a successful publication. - */ - cleanupPending: boolean; -} -export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointHead { - revision: string; - manifest: AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointObject; -} -export interface AgentPublishSessionCheckpointOutput_AgentHistoryCheckpointObject { - digest: string; - bytes: number; +export interface AgentPublishCheckpointOutput_AgentCheckpointWorkspaceNotice { + consistency: AgentPublishCheckpointOutput_AgentCheckpointWorkspaceConsistency; + warning: AgentPublishCheckpointOutput_AgentCheckpointWorkspaceWarning; } export interface AgentReadHistoryCheckpointOutput_AgentHistoryCheckpointHead { revision: string; @@ -2557,11 +2442,6 @@ export interface AgentRestoreSessionCheckpointInput_AgentHistoryCheckpointRead { */ revision: string; } -export interface AgentRestoreSessionCheckpointInput_AgentDirectorySelection { - rootId: string; - segments: string[]; - directoryIdentity: string; -} export interface AgentRestoreSessionCheckpointInput_AgentWorkspacePermissions { access: AgentRestoreSessionCheckpointInput_AgentWorkspaceAccess; requestPermissions: boolean; @@ -2592,99 +2472,6 @@ export interface AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceSna warning: AgentRestoreSessionCheckpointOutput_AgentCheckpointWorkspaceWarning; inventory: AgentRestoreSessionCheckpointOutput_AgentHistoryCheckpointObject; } -export interface AgentSessionCheckpointReceiptInput_RecoveryRef { - source: AgentSessionCheckpointReceiptInput_DeploymentRef; - namespace: AgentSessionCheckpointReceiptInput_InstanceId; - export: string; - method: string; - operation: string; - fingerprint: string; - contract: AgentSessionCheckpointReceiptInput_RecoveryContract; - recovery: AgentSessionCheckpointReceiptInput_RecoveryProtocol; -} -export interface AgentSessionCheckpointReceiptInput_DeploymentRef { - stateDomain: string; - deployment: string; - revision: number; -} -/** - * Contract identity for a journal lookup. The original request retains the - * complete contract; a receipt carries only its digest. - */ -export interface AgentSessionCheckpointReceiptInput_RecoveryContract { - name: string; - version: number; - transport: AgentSessionCheckpointReceiptInput_Transport; - digest: string; -} -export interface AgentSessionCheckpointReceiptInput_AgentSessionCheckpointPublish { - history: AgentSessionCheckpointReceiptInput_AgentHistoryCheckpointPublish; - /** - * Must be explicitly selected. Maka does not fence other applications. - */ - workspaceConsistency: AgentSessionCheckpointReceiptInput_AgentCheckpointWorkspaceConsistency; -} -export interface AgentSessionCheckpointReceiptInput_AgentHistoryCheckpointPublish { - sessionId: string; - expectedRevision: string | null; - /** - * Membership confirmation returned by previewHistoryCheckpoint, not a live revision. - */ - confirmedSubtreeDigest: string; -} -export interface AgentSessionCheckpointReceiptOutput_OperationObservation { - reference: AgentSessionCheckpointReceiptOutput_RecoveryRef; - outcome: AgentSessionCheckpointReceiptOutput_OperationOutcome; - cleanup: AgentSessionCheckpointReceiptOutput_OperationCleanup; -} -export interface AgentSessionCheckpointReceiptOutput_RecoveryRef { - source: AgentSessionCheckpointReceiptOutput_DeploymentRef; - namespace: AgentSessionCheckpointReceiptOutput_InstanceId; - export: string; - method: string; - operation: string; - fingerprint: string; - contract: AgentSessionCheckpointReceiptOutput_RecoveryContract; - recovery: AgentSessionCheckpointReceiptOutput_RecoveryProtocol; -} -export interface AgentSessionCheckpointReceiptOutput_DeploymentRef { - stateDomain: string; - deployment: string; - revision: number; -} -/** - * Contract identity for a journal lookup. The original request retains the - * complete contract; a receipt carries only its digest. - */ -export interface AgentSessionCheckpointReceiptOutput_RecoveryContract { - name: string; - version: number; - transport: AgentSessionCheckpointReceiptOutput_Transport; - digest: string; -} -export interface AgentSessionCheckpointReceiptOutput_AgentSessionCheckpointPublication { - publication: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointPublication; - workspaceConsistency: AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceConsistency; - /** - * Stable warning, present even with a caller attestation. - */ - warning: AgentSessionCheckpointReceiptOutput_AgentCheckpointWorkspaceWarning; -} -export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointPublication { - state: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointState; - /** - * Cleanup failure does not undo a successful publication. - */ - cleanupPending: boolean; -} -export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointHead { - revision: string; - manifest: AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointObject; -} -export interface AgentSessionCheckpointReceiptOutput_AgentHistoryCheckpointObject { - digest: string; - bytes: number; -} export interface AgentSessionCheckpointRestoreReceiptInput_RecoveryRef { source: AgentSessionCheckpointRestoreReceiptInput_DeploymentRef; namespace: AgentSessionCheckpointRestoreReceiptInput_InstanceId; @@ -2716,7 +2503,10 @@ export interface AgentSessionCheckpointRestoreReceiptInput_RecoveryContract { */ export interface AgentSessionCheckpointRestoreReceiptInput_AgentSessionCheckpointRestore { checkpoint: AgentSessionCheckpointRestoreReceiptInput_AgentHistoryCheckpointRead; - parent: AgentSessionCheckpointRestoreReceiptInput_AgentDirectorySelection; + /** + * Absolute parent path. The Host captures and validates its directory identity. + */ + parent: string; permissions: AgentSessionCheckpointRestoreReceiptInput_AgentWorkspacePermissions; backend: AgentSessionCheckpointRestoreReceiptInput_AgentBackendPreference; acceptExternalWritersRisk: boolean; @@ -2728,11 +2518,6 @@ export interface AgentSessionCheckpointRestoreReceiptInput_AgentHistoryCheckpoin */ revision: string; } -export interface AgentSessionCheckpointRestoreReceiptInput_AgentDirectorySelection { - rootId: string; - segments: string[]; - directoryIdentity: string; -} export interface AgentSessionCheckpointRestoreReceiptInput_AgentWorkspacePermissions { access: AgentSessionCheckpointRestoreReceiptInput_AgentWorkspaceAccess; requestPermissions: boolean; diff --git a/platform/event-log/Cargo.toml b/platform/event-log/Cargo.toml index 5a88da492df..79f60c69908 100644 --- a/platform/event-log/Cargo.toml +++ b/platform/event-log/Cargo.toml @@ -24,6 +24,7 @@ publish.workspace = true autotests = false [dependencies] +ignore = "0.4.33" cap-std.workspace = true cap-fs-ext.workspace = true hex.workspace = true diff --git a/platform/event-log/src/bundle.rs b/platform/event-log/src/bundle.rs index 76fd44f85fd..598ce0d5203 100644 --- a/platform/event-log/src/bundle.rs +++ b/platform/event-log/src/bundle.rs @@ -38,6 +38,7 @@ mod relocation; mod stage; mod validation; pub use export::BundleSummary; +pub(crate) use export::{history_cut, require_idle}; pub use reader::inspect; pub use stage::StagedBundle; diff --git a/platform/event-log/src/bundle/export.rs b/platform/event-log/src/bundle/export.rs index 880f83810b7..785fd8956b8 100644 --- a/platform/event-log/src/bundle/export.rs +++ b/platform/event-log/src/bundle/export.rs @@ -132,7 +132,10 @@ async fn events( } } -async fn require_idle(db: &mut SqliteConnection, inventory: &Inventory) -> Result<(), StoreError> { +pub(crate) async fn require_idle( + db: &mut SqliteConnection, + inventory: &Inventory, +) -> Result<(), StoreError> { for session in &inventory.sessions { if crate::sessions::has_active_invocation(db, &session.id).await? { return Err(StoreError::SessionBusy); @@ -151,6 +154,24 @@ async fn require_idle(db: &mut SqliteConnection, inventory: &Inventory) -> Resul Ok(()) } +pub(crate) async fn history_cut( + db: &mut SqliteConnection, + session: &str, +) -> Result { + use sha2::{Digest, Sha256}; + let mut hash = Sha256::new(); + let mut rows = sqlx::query_scalar::<_, i64>( + "SELECT sequence FROM runtime_events WHERE event_session=?1 + UNION SELECT sequence FROM session_history_members WHERE session_id=?1 ORDER BY sequence", + ) + .bind(session) + .fetch(db); + while let Some(sequence) = rows.try_next().await? { + hash.update(sequence_number(sequence)?.to_le_bytes()); + } + Ok(hex::encode(hash.finalize())) +} + async fn catalog( db: &mut SqliteConnection, inventory: &Inventory, diff --git a/platform/event-log/src/bundle/stage.rs b/platform/event-log/src/bundle/stage.rs index 1f63c465ed1..bb3a0759b67 100644 --- a/platform/event-log/src/bundle/stage.rs +++ b/platform/event-log/src/bundle/stage.rs @@ -134,6 +134,24 @@ impl StagedBundle { } } + pub(crate) async fn history_cut(&mut self, session: &str) -> Result { + use futures_util::TryStreamExt; + use sha2::{Digest, Sha256}; + let mut hash = Sha256::new(); + let mut rows = sqlx::query_scalar::<_, i64>( + "SELECT DISTINCT CAST(json_extract(record_json,'$.sequence') AS INTEGER) AS sequence + FROM frames WHERE + (json_extract(record_json,'$.kind')='member' AND json_extract(record_json,'$.session')=?1) + OR (json_extract(record_json,'$.kind')='event' AND + json_extract(json_extract(record_json,'$.json'),'$.invocation.session_id')=?1) + ORDER BY sequence" + ).bind(session).fetch(&mut self.database); + while let Some(sequence) = rows.try_next().await? { + hash.update(crate::sequence_number(sequence)?.to_le_bytes()); + } + Ok(hex::encode(hash.finalize())) + } + pub fn summary(&self) -> &BundleSummary { &self.summary } diff --git a/platform/event-log/src/checkpoints.rs b/platform/event-log/src/checkpoints.rs index bf9fe25e0d2..f96142f8e49 100644 --- a/platform/event-log/src/checkpoints.rs +++ b/platform/event-log/src/checkpoints.rs @@ -53,7 +53,6 @@ pub struct PublicationRequest { /// Confirmation of catalog membership, not a live revision precondition. pub confirmed_subtree_digest: String, /// Trusted assembly supplies the source; never a model-authored path. - #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace: Option, } @@ -66,39 +65,23 @@ pub struct ObjectRef { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", deny_unknown_fields)] -pub enum Manifest { - #[serde(rename = "native_history_v1")] - NativeHistory { - root_id: String, - scope: Scope, - inventory: Inventory, - /// MAKA-SESSION version 2; no TS tar.zst compatibility is implied. - bundle: ObjectRef, - /// The native format's own framed-stream checksum. - transfer_digest: String, - }, - #[serde(rename = "native_session_v1")] - NativeSession { - root_id: String, - scope: Scope, - inventory: Inventory, - bundle: ObjectRef, - transfer_digest: String, - workspace: WorkspaceSnapshot, - }, +#[serde(deny_unknown_fields)] +pub struct Manifest { + pub root_id: String, + pub scope: Scope, + pub inventory: Inventory, + /// MAKA-SESSION version 2; no TS tar.zst compatibility is implied. + pub bundle: ObjectRef, + pub transfer_digest: String, + pub history_cut: String, + pub workspace: Option, } impl Manifest { pub fn bundle(&self) -> &ObjectRef { - match self { - Self::NativeHistory { bundle, .. } | Self::NativeSession { bundle, .. } => bundle, - } + &self.bundle } pub fn workspace(&self) -> Option<&WorkspaceSnapshot> { - match self { - Self::NativeHistory { .. } => None, - Self::NativeSession { workspace, .. } => Some(workspace), - } + self.workspace.as_ref() } } diff --git a/platform/event-log/src/checkpoints/load.rs b/platform/event-log/src/checkpoints/load.rs index e5101dd6a03..77efe272fe3 100644 --- a/platform/event-log/src/checkpoints/load.rs +++ b/platform/event-log/src/checkpoints/load.rs @@ -35,7 +35,7 @@ pub enum LoadState { Ready { session_ids: Vec, through: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] + transfer_digest: String, workspace: Option, }, Unavailable, diff --git a/platform/event-log/src/checkpoints/restore.rs b/platform/event-log/src/checkpoints/restore.rs index b169216d1aa..221c8c6fd32 100644 --- a/platform/event-log/src/checkpoints/restore.rs +++ b/platform/event-log/src/checkpoints/restore.rs @@ -33,6 +33,7 @@ pub struct RestoreOperation { pub struct PreparedRestore { pub head: Head, pub source_revision: u64, + pub history_cut: String, pub workspace: WorkspaceSnapshot, pub path: std::path::PathBuf, } diff --git a/platform/event-log/src/lib.rs b/platform/event-log/src/lib.rs index 7c5cc86a5f9..eefc4344b81 100644 --- a/platform/event-log/src/lib.rs +++ b/platform/event-log/src/lib.rs @@ -207,6 +207,41 @@ enum OpenPurpose { } impl EventLog { + /// A completed checkpoint inspection is immutable and never enters execution. + /// The caller holds its shared checkpoint gate through the physical close. + async fn open_checkpoint_inspection( + path: &Path, + root: Arc, + through: u64, + ) -> Result { + let mut wal = path.as_os_str().to_os_string(); + wal.push("-wal"); + match std::fs::metadata(Path::new(&wal)) { + Ok(metadata) if metadata.len() != 0 => return Err(StoreError::WriterBusy), + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error.into()), + } + let connection = OwnedConnection::open( + SqliteConnectOptions::new() + .filename(path) + .create_if_missing(false) + .read_only(true) + .immutable(true) + .query_planner_stability(true), + ConnectionAuthority::Root(root.clone()), + application_inspection::initialize, + ) + .await?; + Ok(Self { + expected_session: None, + connection, + root_owner: Some(root), + commits: commits::CommitHub::new(through), + shell_changes: tokio::sync::broadcast::channel(256).0, + }) + } + /// Observe canonical SQLite actors. pub async fn sqlite_resource_stats( &self, diff --git a/platform/event-log/src/root_store/checkpoints.rs b/platform/event-log/src/root_store/checkpoints.rs index ac1c492d775..bdfb75c952d 100644 --- a/platform/event-log/src/root_store/checkpoints.rs +++ b/platform/event-log/src/root_store/checkpoints.rs @@ -214,18 +214,12 @@ async fn checkout( .filter(|head| head.revision == revision) .ok_or_else(|| invalid("history checkpoint revision unavailable"))?; let manifest = objects.manifest(&head.manifest)?; - let (Manifest::NativeHistory { + let Manifest { root_id, scope: origin, inventory, .. - } - | Manifest::NativeSession { - root_id, - scope: origin, - inventory, - .. - }) = &manifest; + } = &manifest; if root_id != objects.root_id() || origin != &scope || inventory.root_session_id != session { return Err(invalid("history checkpoint origin differs")); } @@ -269,26 +263,37 @@ async fn capture( drop(file); let bundle = objects.publish(&objects.staging(request, "bundle")?)?; boundary("payload"); - let manifest = if let Some(source) = &request.publication.workspace { - let workspace = workspace::capture(objects, request, source)?; - Manifest::NativeSession { - root_id: objects.root_id().into(), - scope: request.scope.clone(), - inventory: summary.inventory, - bundle, - transfer_digest: summary.digest, - workspace, - } - } else { - Manifest::NativeHistory { - root_id: objects.root_id().into(), - scope: request.scope.clone(), - inventory: summary.inventory, - bundle, - transfer_digest: summary.digest, + let mut staged = StagedBundle::read(tokio::fs::File::from_std(objects.read(&bundle)?)) + .await + .map_err(bundle_error)?; + let validated = async { + staged.validate_history().await.map_err(bundle_error)?; + if staged.summary().inventory != summary.inventory + || staged.summary().digest != summary.digest + { + return Err(invalid("checkpoint captured bundle differs")); } + staged.history_cut(&request.session_id).await + } + .await; + let closed = staged.close().await.map_err(bundle_error); + let history_cut = validated?; + closed?; + let workspace = request + .publication + .workspace + .as_ref() + .map(|source| workspace::capture(objects, request, source)) + .transpose()?; + let manifest = Manifest { + root_id: objects.root_id().into(), + scope: request.scope.clone(), + inventory: summary.inventory, + bundle, + transfer_digest: summary.digest, + history_cut, + workspace, }; - validate_bundle(objects, &manifest).await?; objects.publish_manifest(request, &manifest) } @@ -296,18 +301,12 @@ async fn validate_bundle(objects: &Objects, manifest: &Manifest) -> Result<(), S if let Some(workspace) = manifest.workspace() { workspace::verify(objects, workspace)?; } - let (Manifest::NativeHistory { - inventory, - bundle, - transfer_digest, - .. - } - | Manifest::NativeSession { + let Manifest { inventory, bundle, transfer_digest, .. - }) = manifest; + } = manifest; let file = tokio::fs::File::from_std(objects.read(bundle)?); let mut staged = StagedBundle::read(file).await.map_err(bundle_error)?; let result = async { @@ -317,7 +316,11 @@ async fn validate_bundle(objects: &Objects, manifest: &Manifest) -> Result<(), S { return Err(invalid("checkpoint manifest and bundle differ")); } - staged.validate_history().await.map_err(bundle_error) + staged.validate_history().await.map_err(bundle_error)?; + if staged.history_cut(&inventory.root_session_id).await? != manifest.history_cut { + return Err(invalid("checkpoint history cut differs from bundle")); + } + Ok(()) } .await; let closed = staged.close().await.map_err(bundle_error); @@ -330,18 +333,12 @@ async fn verify( reference: &ObjectRef, ) -> Result<(), StoreError> { let manifest = objects.manifest(reference)?; - let (Manifest::NativeHistory { + let Manifest { root_id, scope, inventory, .. - } - | Manifest::NativeSession { - root_id, - scope, - inventory, - .. - }) = &manifest; + } = &manifest; if root_id != objects.root_id() || scope != &request.scope || inventory.root_session_id != request.session_id @@ -418,6 +415,33 @@ async fn worker( gates: Arc, action: F, ) -> Result +where + T: Send + 'static, + F: FnOnce(EventLog, Objects) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, +{ + physical_worker(log, gates, Objects::open, action).await +} + +async fn read_worker( + log: EventLog, + gates: Arc, + action: F, +) -> Result +where + T: Send + 'static, + F: FnOnce(EventLog, Objects) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, +{ + physical_worker(log, gates, Objects::existing, action).await +} + +async fn physical_worker( + log: EventLog, + gates: Arc, + open: fn(Arc) -> Result, + action: F, +) -> Result where T: Send + 'static, F: FnOnce(EventLog, Objects) -> Fut + Send + 'static, @@ -439,7 +463,7 @@ where let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; - let objects = Objects::open(root)?; + let objects = open(root)?; runtime.block_on(action(log, objects)) })(); work.complete = true; diff --git a/platform/event-log/src/root_store/checkpoints/load.rs b/platform/event-log/src/root_store/checkpoints/load.rs index 66e6e0bbe4c..99d832e54a4 100644 --- a/platform/event-log/src/root_store/checkpoints/load.rs +++ b/platform/event-log/src/root_store/checkpoints/load.rs @@ -130,28 +130,48 @@ impl HistoryCheckpoints { .await } - pub async fn loaded_artifact( + /// Stream through one read-only inspection connection, with bounded chunks. + pub async fn stream_loaded_artifact( &self, operation: Uuid, session: String, artifact: String, offset: u64, - ) -> Result< - Option<( - maka_agent_facts::artifact::Artifact, - crate::artifacts::ArtifactChunk, - )>, - StoreError, - > { + mut emit: F, + ) -> Result<(), StoreError> + where + F: FnMut( + Option<( + maka_agent_facts::artifact::Artifact, + u64, + crate::artifacts::ArtifactChunk, + )>, + ) -> Fut + + Send + + 'static, + Fut: Future> + Send + 'static, + { self.with_loaded(operation, session.clone(), move |log, _| async move { let Some(record) = log.get_artifact(&session, &artifact).await?.record else { - return Ok(None); + return emit(None).await; }; - let chunk = log - .read_artifact_chunk(&session, &artifact, offset, 32 * 1024) - .await? - .ok_or_else(|| invalid("inspection artifact payload missing"))?; - Ok(Some((record, chunk))) + let mut offset = offset; + loop { + let chunk = log + .read_artifact_chunk(&session, &artifact, offset, 32 * 1024) + .await? + .ok_or_else(|| invalid("inspection artifact payload missing"))?; + let next = offset + chunk.bytes.len() as u64; + let done = next >= chunk.total_bytes; + if !done && next == offset { + return Err(invalid("inspection artifact made no progress")); + } + emit(Some((record.clone(), offset, chunk))).await?; + if done { + return Ok(()); + } + offset = next; + } }) .await } @@ -170,9 +190,9 @@ impl HistoryCheckpoints { let key = load_key(&self.scope, self.session.id(), operation)?; let log = self.session.scoped().await?; let gates = self.session.gates.clone(); - worker(log, gates.clone(), move |log, objects| async move { + read_worker(log, gates.clone(), move |log, objects| async move { let _pin = gates - .write([gates::Key::CheckpointLoad(key.clone())]) + .read([gates::Key::CheckpointLoad(key.clone())]) .await?; let (request, loaded) = read(&log, key) .await? @@ -180,6 +200,7 @@ impl HistoryCheckpoints { let LoadState::Ready { ref session_ids, through, + ref transfer_digest, .. } = loaded.state else { @@ -188,9 +209,20 @@ impl HistoryCheckpoints { if !session_ids.contains(&session) { return Err(StoreError::SessionNotFound); } - let target = destination(&objects, &request, false).await?; + let path = objects.inspection(&request.key()?, false)?; + let target = Arc::new( + EventLog::open_checkpoint_inspection(&path, objects.owner.clone(), through).await?, + ); let result = async { - verify_receipt(&target, &objects, &request, &loaded).await?; + let receipt = target + .bundle_import_receipt(transfer_digest, &request.binding()?) + .await? + .ok_or_else(|| invalid("checkpoint inspection import receipt missing"))?; + if receipt.root_session_id != request.session_id + || &receipt.session_ids != session_ids + { + return Err(invalid("checkpoint inspection inventory differs")); + } action(target.clone(), through).await } .await; @@ -354,18 +386,12 @@ fn manifest( loaded: &LoadedCheckpoint, ) -> Result { let value = objects.manifest(&loaded.head.manifest)?; - let (Manifest::NativeHistory { - root_id, - scope, - inventory, - .. - } - | Manifest::NativeSession { + let Manifest { root_id, scope, inventory, .. - }) = &value; + } = &value; if loaded.head.revision != request.load.revision || root_id != objects.root_id() || scope != &request.scope @@ -381,16 +407,11 @@ async fn verify_receipt( request: &LoadRequest, loaded: &LoadedCheckpoint, ) -> Result<(), StoreError> { - let (Manifest::NativeHistory { + let Manifest { transfer_digest, inventory, .. - } - | Manifest::NativeSession { - transfer_digest, - inventory, - .. - }) = manifest(objects, request, loaded)?; + } = manifest(objects, request, loaded)?; let receipt = target .bundle_import_receipt(&transfer_digest, &request.binding()?) .await? @@ -417,18 +438,12 @@ async fn resume( let workspace = source.workspace().cloned(); if let Some(snapshot) = &workspace { super::workspace::verify(objects, snapshot)?; - objects.inspection_workspace(&request.key()?, snapshot)?; - } - let (Manifest::NativeHistory { - inventory, - transfer_digest, - .. } - | Manifest::NativeSession { + let Manifest { inventory, transfer_digest, .. - }) = &source; + } = &source; let target = destination(objects, request, true).await?; let result = async { if target.bundle_import_receipt(transfer_digest, &request.binding()?).await?.is_none() { @@ -465,6 +480,7 @@ async fn resume( Ok(LoadState::Ready { session_ids: inventory.sessions.iter().map(|s| s.id.clone()).collect(), through: target.committed_sequence().await?, + transfer_digest: transfer_digest.clone(), workspace, }) }.await; diff --git a/platform/event-log/src/root_store/checkpoints/objects.rs b/platform/event-log/src/root_store/checkpoints/objects.rs index 73e61ba265c..2ac89b1f922 100644 --- a/platform/event-log/src/root_store/checkpoints/objects.rs +++ b/platform/event-log/src/root_store/checkpoints/objects.rs @@ -47,6 +47,12 @@ impl Objects { File::open(owner.canonical_path())?.sync_all()?; Ok(Self { owner, directory }) } + pub(super) fn existing(owner: Arc) -> Result { + let directory = owner.canonical_path().join("history-checkpoints"); + let objects = Self { owner, directory }; + objects.validate()?; + Ok(objects) + } fn validate(&self) -> Result<(), StoreError> { self.owner.validate_current()?; for path in [ @@ -179,80 +185,6 @@ impl Objects { pub(super) fn root_id(&self) -> &str { self.owner.root_id() } - - pub(super) fn inspection_workspace( - &self, - key: &str, - snapshot: &crate::checkpoints::WorkspaceSnapshot, - ) -> Result { - self.validate()?; - if !crate::checkpoints::valid_digest(key) { - return Err(invalid("invalid inspection key")); - } - let base = self.directory.join("inspections"); - root::private_directory(&base)?; - let marker = base.join(format!("{key}.workspace-owner")); - let bytes = serde_json::to_vec(&(self.root_id(), key, snapshot))?; - // Publish the small ownership marker atomically; a killed write must not - // leave a truncated marker that poisons recovery of the original load. - let preparing = base.join(format!("{key}.workspace-owner-preparing")); - match fs::symlink_metadata(&preparing) { - Ok(m) if m.is_file() => fs::remove_file(&preparing)?, - Ok(_) => { - return Err(invalid( - "checkpoint workspace marker staging is not regular", - )); - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e.into()), - } - let mut options = OpenOptions::new(); - options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600).custom_flags(libc::O_NOFOLLOW); - } - let mut file = options.open(&preparing)?; - file.write_all(&bytes)?; - file.sync_all()?; - drop(file); - match fs::hard_link(&preparing, &marker) { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { - let mut file = open_regular(&marker)?; - let mut actual = Vec::new(); - Read::by_ref(&mut file) - .take(bytes.len() as u64 + 1) - .read_to_end(&mut actual)?; - if actual != bytes { - return Err(invalid("checkpoint workspace owner differs")); - } - } - Err(e) => return Err(e.into()), - } - fs::remove_file(preparing)?; - File::open(&base)?.sync_all()?; - let target = base.join(format!("{key}.workspace")); - if target.try_exists()? { - if !fs::symlink_metadata(&target)?.is_dir() { - return Err(invalid("checkpoint workspace is not a directory")); - } - return Ok(target); - } - let staging = base.join(format!("{key}.workspace-preparing")); - match fs::symlink_metadata(&staging) { - Ok(m) if m.is_dir() => fs::remove_dir_all(&staging)?, - Ok(_) => return Err(invalid("checkpoint workspace staging is not owned")), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e.into()), - } - root::private_directory(&staging)?; - super::workspace::materialize(self, snapshot, &staging)?; - fs::rename(&staging, &target)?; - File::open(&base)?.sync_all()?; - Ok(target) - } } fn open_regular(path: &Path) -> Result { let mut options = OpenOptions::new(); diff --git a/platform/event-log/src/root_store/checkpoints/restore.rs b/platform/event-log/src/root_store/checkpoints/restore.rs index 729f0fe4a3a..9b27849b4a5 100644 --- a/platform/event-log/src/root_store/checkpoints/restore.rs +++ b/platform/event-log/src/root_store/checkpoints/restore.rs @@ -94,6 +94,19 @@ impl Commit { { return Err(invalid("checkpoint restore head moved")); } + let inventory = crate::bundle::Inventory { + root_session_id: self.request.session.clone(), + subtree_digest: String::new(), + sessions: vec![crate::bundle::Session { + id: self.request.session.clone(), + revision: self.prepared.source_revision, + }], + }; + crate::bundle::require_idle(db, &inventory).await?; + if crate::bundle::history_cut(db, &self.request.session).await? != self.prepared.history_cut + { + return Err(invalid("live Session history advanced beyond checkpoint")); + } let row: Option<(String, String, Option, Option)> = sqlx::query_as("SELECT request_json,prepared_json,directory_identity_json,result_json FROM checkpoint_workspace_restores WHERE restore_key=?").bind(&self.key).fetch_optional(db).await?; if row != Some(( @@ -163,15 +176,24 @@ impl HistoryCheckpoints { return Ok(prior.prepared); } let checkout = checkout(&log, &objects, request.scope.clone(), request.session.clone(), request.operation.revision).await?; - let Manifest::NativeSession { inventory, workspace, .. } = checkout.manifest else { return Err(invalid("checkpoint has no workspace")); }; + let Manifest { inventory, workspace, history_cut, .. } = checkout.manifest; + let workspace = workspace.ok_or_else(|| invalid("checkpoint has no workspace"))?; if inventory.sessions.len() != 1 || inventory.sessions[0].id != request.session { return Err(invalid("checkpoint restore requires one ordinary Session")); } let current = log.get_session::(&request.session).await?.ok_or(StoreError::SessionNotFound)?; - if current.archived || current.revision != inventory.sessions[0].revision { return Err(invalid("live Session advanced beyond checkpoint")); } + if current.archived { return Err(invalid("archived Session cannot be restored")); } + let expected = history_cut.clone(); + log.connection.read(move |db| Box::pin(async move { + crate::bundle::require_idle(db, &inventory).await?; + if crate::bundle::history_cut(db, &inventory.root_session_id).await? != expected { + return Err(invalid("live Session history advanced beyond checkpoint")); + } + Ok(()) + })).await?; let parent = request.operation.parent.canonicalize()?; let directory = cap_std::fs::Dir::open_ambient_dir(&parent, cap_std::ambient_authority())?; if directory_identity(&directory)? != request.operation.parent_identity { return Err(invalid("restore parent directory changed")); } if parent.starts_with(objects.owner.canonical_path()) { return Err(invalid("restore destination overlaps private Root")); } - let prepared = PreparedRestore { head: checkout.head, source_revision: current.revision, workspace, path: parent.join(format!("maka-restored-{}", request.key()?)) }; + let prepared = PreparedRestore { head: checkout.head, source_revision: current.revision, history_cut, workspace, path: parent.join(format!("maka-restored-{}", request.key()?)) }; let key = request.key()?; let encoded = serde_json::to_string(&request)?; let data = serde_json::to_string(&prepared)?; diff --git a/platform/event-log/src/root_store/checkpoints/tests/load.rs b/platform/event-log/src/root_store/checkpoints/tests/load.rs index c21f5428dee..bb6ab1f4a10 100644 --- a/platform/event-log/src/root_store/checkpoints/tests/load.rs +++ b/platform/event-log/src/root_store/checkpoints/tests/load.rs @@ -270,3 +270,60 @@ async fn load_process_death_recovers_original_target_at_each_publication_boundar store.close().await.unwrap(); } } + +#[tokio::test] +async fn inspection_reads_share_admission_and_do_not_take_a_writer_lease() { + let dir = tempfile::tempdir().unwrap(); + let (store, handle, publish) = fixture(dir.path()).await; + let head = published(&handle.publish(publish).await.unwrap()); + let input = request(head.revision); + handle.load_exact(input.clone()).await.unwrap(); + let key = LoadRequest { + scope: scope(), + session_id: "source".into(), + load: input.clone(), + } + .key() + .unwrap(); + let objects = Objects::existing(store.owned_root().unwrap()).unwrap(); + let database = objects.inspection(&key, false).unwrap(); + let mut lock = database.as_os_str().to_os_string(); + lock.push(".writer.lock"); + let lease = crate::root::FileLease::acquire(Path::new(&lock)).unwrap(); + let entered = Arc::new(tokio::sync::Notify::new()); + let release = Arc::new(tokio::sync::Notify::new()); + let reader = handle.clone(); + let (e, r) = (entered.clone(), release.clone()); + let task = tokio::spawn(async move { + reader + .stream_loaded_artifact( + input.operation, + "source".into(), + "missing".into(), + 0, + move |value| { + let (e, r) = (e.clone(), r.clone()); + async move { + assert!(value.is_none()); + e.notify_one(); + r.notified().await; + Ok(()) + } + }, + ) + .await + }); + tokio::time::timeout(Duration::from_secs(10), entered.notified()) + .await + .unwrap(); + let read = tokio::time::timeout( + Duration::from_secs(10), + handle.loaded_history(input.operation, "source".into(), Position::Tail, false), + ) + .await; + release.notify_one(); + task.await.unwrap().unwrap(); + read.unwrap().unwrap(); + drop(lease); + store.close().await.unwrap(); +} diff --git a/platform/event-log/src/root_store/checkpoints/tests/workspace.rs b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs index 7d0372f3d2a..bc4c032c65b 100644 --- a/platform/event-log/src/root_store/checkpoints/tests/workspace.rs +++ b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs @@ -78,19 +78,9 @@ async fn workspace_material_is_immutable_and_load_preserves_conditional_guarante .canonical_path() .join("history-checkpoints/inspections") .join(format!("{key}.workspace")); - assert_eq!( - std::fs::read_to_string(path.join("src/main.rs")).unwrap(), - "original" - ); - assert!(path.join("empty").is_dir()); - assert!(!path.join(".git").exists()); - assert_ne!( - std::fs::metadata(path.join("src/main.rs")) - .unwrap() - .permissions() - .mode() - & 0o111, - 0 + assert!( + !path.exists(), + "inspection must not copy workspace payloads" ); assert_eq!(published(&handle.publish(request).await.unwrap()), head); store.close().await.unwrap(); @@ -109,13 +99,12 @@ async fn workspace_material_is_immutable_and_load_preserves_conditional_guarante #[tokio::test] async fn unsafe_workspace_material_cannot_publish_a_head() { - for kind in ["secret", "symlink", "fifo", "overlap"] { + for kind in ["symlink", "fifo", "overlap"] { let dir = tempfile::tempdir().unwrap(); let source = dir.path().join("project"); std::fs::create_dir(&source).unwrap(); let (store, handle, mut request) = fixture(dir.path()).await; match kind { - "secret" => std::fs::write(source.join(".env"), "secret").unwrap(), "symlink" => std::os::unix::fs::symlink(dir.path(), source.join("link")).unwrap(), "fifo" => { assert!( @@ -379,11 +368,61 @@ async fn restore_receipt_is_atomic_with_binding_and_survives_later_source_change } #[tokio::test] async fn restore_rejects_head_race_and_changed_destination_without_rebinding() { - for race in ["head", "files", "extra", "parent", "symlink", "replacement"] { + for race in [ + "head", + "history", + "busy", + "files", + "extra", + "parent", + "symlink", + "replacement", + ] { let directory = tempfile::tempdir().unwrap(); let (store, handle, mut publish, operation, prepared, configuration) = restore_fixture(directory.path()).await; match race { + "history" | "busy" => { + use maka_agent_facts::event::{ + EventWrite, Fact, Invocation, InvocationInput, InvocationOutcome, RuntimeEvent, + }; + let log = handle.session.scoped().await.unwrap(); + let invocation = Invocation { + session_id: "source".into(), + turn_id: "new".into(), + run_id: "new".into(), + invocation_id: "new".into(), + }; + log.append( + &EventWrite::plain(RuntimeEvent::new( + invocation.clone(), + Fact::InvocationOpened { + configuration: None, + input: InvocationInput::Message { + content: "new history".into(), + source_messages: vec![], + request_fingerprint: None, + }, + }, + )) + .unwrap(), + ) + .await + .unwrap(); + if race == "history" { + log.append( + &EventWrite::plain(RuntimeEvent::new( + invocation, + Fact::InvocationEnded { + outcome: InvocationOutcome::Completed, + }, + )) + .unwrap(), + ) + .await + .unwrap(); + } + } "head" => { publish.operation = Uuid::new_v4(); publish.expected_revision = Some(operation.revision); @@ -406,6 +445,11 @@ async fn restore_rejects_head_race_and_changed_destination_without_rebinding() { } _ => std::fs::write(prepared.path.join("extra"), "unrecorded").unwrap(), } + let before = store + .get_session::("source") + .await + .unwrap() + .unwrap(); assert!( handle .commit_workspace_restore(operation.clone(), configuration, 2) @@ -413,15 +457,13 @@ async fn restore_rejects_head_race_and_changed_destination_without_rebinding() { .is_err(), "accepted {race} race" ); - assert_eq!( - store - .get_session::("source") - .await - .unwrap() - .unwrap() - .revision, - prepared.source_revision - ); + let after = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + assert_eq!(after.revision, before.revision); + assert_eq!(after.configuration, before.configuration); assert!( handle .workspace_restore_receipt( @@ -502,3 +544,141 @@ async fn restore_cannot_commit_a_copy_without_a_durable_completion_record() { ); store.close().await.unwrap(); } + +#[tokio::test] +async fn workspace_ignores_build_outputs_and_secrets_but_keeps_certificate_fixtures() { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + for child in [".git/info", "target", ".venv", "nested", "fixtures"] { + std::fs::create_dir_all(source.join(child)).unwrap(); + } + std::fs::write( + source.join(".gitignore"), + "target/\n.venv/\n*.log\n!keep.log\n", + ) + .unwrap(); + std::fs::write(source.join(".git/info/exclude"), "local-only\n").unwrap(); + std::fs::write(source.join("nested/.gitignore"), "ignored\n").unwrap(); + for file in [ + "target/output", + ".venv/bin", + "build.log", + "local-only", + "nested/ignored", + ".env", + "fixtures/test.pem", + "fixtures/test.key", + "keep.log", + "nested/keep", + ] { + std::fs::write(source.join(file), file).unwrap(); + } + std::os::unix::fs::symlink("/nonexistent", source.join("target/link")).unwrap(); + let (store, handle, mut request) = fixture(dir.path()).await; + request.workspace = Some(WorkspaceCapture { + root: source, + identity: "workspace".into(), + consistency: WorkspaceConsistency::Unverified, + }); + let head = published(&handle.publish(request).await.unwrap()); + let checkout = handle.checkout_exact(head.revision).await.unwrap(); + let objects = Objects::open(store.owned_root().unwrap()).unwrap(); + let inventory: serde_json::Value = serde_json::from_reader( + objects + .read(&checkout.manifest.workspace().unwrap().inventory) + .unwrap(), + ) + .unwrap(); + let paths = inventory["entries"] + .as_array() + .unwrap() + .iter() + .map(|entry| entry["path"].as_str().unwrap()) + .collect::>(); + for retained in [ + "fixtures/test.pem", + "fixtures/test.key", + "keep.log", + "nested/keep", + ] { + assert!(paths.contains(retained), "missing {retained}: {paths:?}"); + } + for omitted in [ + "target", + ".venv", + "build.log", + "local-only", + "nested/ignored", + ".env", + ".git", + ] { + assert!(!paths.contains(omitted), "unexpected {omitted}: {paths:?}"); + } + assert!(inventory.get("version").is_none()); + store.close().await.unwrap(); +} + +#[tokio::test] +async fn restore_allows_metadata_changes_and_repeated_restore_of_the_same_history() { + let directory = tempfile::tempdir().unwrap(); + let (store, handle, _, original, _, mut configuration) = + restore_fixture(directory.path()).await; + let current = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + store + .update_session_metadata( + "source", + current.revision, + |value: &mut serde_json::Value| { + value["title"] = json!("renamed after checkpoint"); + Ok(()) + }, + ) + .await + .unwrap(); + let mut request = original.clone(); + request.operation = Uuid::new_v4(); + request.fingerprint = "e".repeat(64); + let prepared = handle + .prepare_workspace_restore(request.clone()) + .await + .unwrap(); + assert_eq!( + std::fs::read_to_string(prepared.path.join("file")).unwrap(), + "frozen" + ); + configuration["workspace"] = serde_json::to_value( + store + .attach_workspace("fixture", &json!({"path":prepared.path})) + .await + .unwrap(), + ) + .unwrap(); + handle + .commit_workspace_restore(request, configuration.clone(), 3) + .await + .unwrap(); + let mut again = original; + again.operation = Uuid::new_v4(); + again.fingerprint = "f".repeat(64); + let second = handle + .prepare_workspace_restore(again.clone()) + .await + .unwrap(); + assert_ne!(prepared.path, second.path); + configuration["workspace"] = serde_json::to_value( + store + .attach_workspace("fixture", &json!({"path":second.path})) + .await + .unwrap(), + ) + .unwrap(); + handle + .commit_workspace_restore(again, configuration, 4) + .await + .unwrap(); + store.close().await.unwrap(); +} diff --git a/platform/event-log/src/root_store/checkpoints/workspace.rs b/platform/event-log/src/root_store/checkpoints/workspace.rs index bcc256dfce5..c32e242eb15 100644 --- a/platform/event-log/src/root_store/checkpoints/workspace.rs +++ b/platform/event-log/src/root_store/checkpoints/workspace.rs @@ -32,14 +32,9 @@ use std::{ #[cfg(test)] thread_local! { pub(super) static AFTER_SCAN: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; } -const MAX_ENTRIES: usize = 20_000; -const MAX_BYTES: u64 = 2 * 1024 * 1024 * 1024; -const MAX_INVENTORY: u64 = 16 * 1024 * 1024; - #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct Inventory { - version: u32, entries: Vec, } @@ -77,9 +72,6 @@ fn secret(name: &str) -> bool { name.as_str(), ".ssh" | ".aws" | ".gnupg" | "credentials.json" | "id_rsa" | "id_ed25519" ) - || name.ends_with(".key") - || name.ends_with(".pem") - || name.ends_with(".p12") } fn stamp(meta: &cap_std::fs::Metadata) -> Result { @@ -108,50 +100,52 @@ fn stamp(meta: &cap_std::fs::Metadata) -> Result { } } -fn scan( - dir: &Dir, - prefix: &str, - output: &mut BTreeMap, - depth: usize, -) -> Result<(), StoreError> { - if depth > 64 { - return Err(invalid("checkpoint workspace depth limit")); - } - for entry in dir.entries()? { - let entry = entry?; - let name = entry - .file_name() - .into_string() - .map_err(|_| invalid("non-UTF8 checkpoint workspace path"))?; - if excluded(&name) { - continue; +fn scan(dir: &Dir, root: &Path) -> Result, StoreError> { + // The repository controls what belongs to its workspace. The capability + // handle is still used to open every selected path without following links. + let walk = ignore::WalkBuilder::new(root) + .hidden(false) + .parents(false) + .git_global(false) + .require_git(false) + .follow_links(false) + .filter_entry(|entry| { + entry.depth() == 0 + || entry + .file_name() + .to_str() + .is_none_or(|name| !excluded(name) && !secret(name)) + }) + .build(); + let mut output = BTreeMap::new(); + for entry in walk { + let entry = + entry.map_err(|error| invalid(format!("checkpoint workspace scan: {error}")))?; + if let Some(error) = entry.error() { + return Err(invalid(format!( + "checkpoint workspace ignore rules: {error}" + ))); } - if secret(&name) { - return Err(invalid( - "checkpoint workspace contains an excluded secret path", - )); - } - let path = if prefix.is_empty() { - name.clone() - } else { - format!("{prefix}/{name}") - }; - relative(&path)?; - if output.len() >= MAX_ENTRIES { - return Err(invalid("checkpoint workspace entry limit")); + if entry.depth() == 0 { + continue; } - let metadata = dir.symlink_metadata(&name)?; + let path = entry + .path() + .strip_prefix(root) + .map_err(|_| invalid("checkpoint workspace path escaped root"))? + .to_str() + .ok_or_else(|| invalid("non-UTF8 checkpoint workspace path"))?; + relative(path)?; + let (parent, name) = parent(dir, path)?; + let metadata = parent.symlink_metadata(&name)?; if !metadata.is_dir() && !metadata.is_file() { return Err(invalid( "checkpoint workspace links and special files are unsupported", )); } - output.insert(path.clone(), stamp(&metadata)?); - if metadata.is_dir() { - scan(&dir.open_dir_nofollow(&name)?, &path, output, depth + 1)?; - } + output.insert(path.to_owned(), stamp(&metadata)?); } - Ok(()) + Ok(output) } fn parent(dir: &Dir, path: &str) -> Result<(Dir, String), StoreError> { @@ -180,8 +174,7 @@ pub(super) fn capture( } let dir = Dir::open_ambient_dir(&source_path, cap_std::ambient_authority())?; let before_root = stamp(&dir.dir_metadata()?)?; - let mut before = BTreeMap::new(); - scan(&dir, "", &mut before, 0)?; + let before = scan(&dir, &source_path)?; #[cfg(test)] AFTER_SCAN.with(|hook| { if let Some(action) = hook.borrow_mut().take() { @@ -189,12 +182,7 @@ pub(super) fn capture( } }); let mut entries = Vec::new(); - let mut total = 0u64; - let start = std::time::Instant::now(); for (path, expected) in &before { - if start.elapsed() > std::time::Duration::from_secs(120) { - return Err(invalid("checkpoint workspace capture deadline")); - } objects.owner.validate_current()?; let (parent, name) = parent(&dir, path)?; let metadata = parent.symlink_metadata(&name)?; @@ -220,16 +208,12 @@ pub(super) fn capture( let mut copied = 0u64; let mut buffer = [0u8; 65536]; loop { - if start.elapsed() > std::time::Duration::from_secs(120) { - return Err(invalid("checkpoint workspace capture deadline")); - } let n = file.read(&mut buffer)?; if n == 0 { break; } copied += n as u64; - total += n as u64; - if total > MAX_BYTES || copied > metadata.len() { + if copied > metadata.len() { return Err(invalid("checkpoint workspace size limit or source change")); } staged.write_all(&buffer[..n])?; @@ -258,8 +242,7 @@ pub(super) fn capture( executable, }); } - let mut after = BTreeMap::new(); - scan(&dir, "", &mut after, 0)?; + let after = scan(&dir, &source_path)?; let named = Dir::open_ambient_dir(&source.root, cap_std::ambient_authority())?; if before != after || before_root != stamp(&dir.dir_metadata()?)? @@ -267,13 +250,7 @@ pub(super) fn capture( { return Err(invalid("checkpoint workspace changed during capture")); } - let encoded = serde_json::to_vec(&Inventory { - version: 1, - entries, - })?; - if encoded.len() as u64 > MAX_INVENTORY { - return Err(invalid("checkpoint workspace inventory limit")); - } + let encoded = serde_json::to_vec(&Inventory { entries })?; let mut file = objects.create(request, "workspace")?; file.write_all(&encoded)?; file.sync_all()?; @@ -289,13 +266,7 @@ pub(super) fn inventory( objects: &Objects, snapshot: &WorkspaceSnapshot, ) -> Result, StoreError> { - if snapshot.inventory.bytes > MAX_INVENTORY { - return Err(invalid("checkpoint workspace inventory limit")); - } let inventory: Inventory = serde_json::from_reader(objects.read(&snapshot.inventory)?)?; - if inventory.version != 1 || inventory.entries.len() > MAX_ENTRIES { - return Err(invalid("unsupported checkpoint workspace inventory")); - } let mut paths = BTreeSet::new(); let mut directories = BTreeSet::new(); let mut total = 0u64; @@ -321,9 +292,6 @@ pub(super) fn inventory( total = total .checked_add(object.bytes) .ok_or_else(|| invalid("checkpoint workspace size overflow"))?; - if total > MAX_BYTES { - return Err(invalid("checkpoint workspace size limit")); - } } } } @@ -339,15 +307,7 @@ pub(super) fn verify(objects: &Objects, snapshot: &WorkspaceSnapshot) -> Result< Ok(()) } -/// The caller supplies an exclusively owned, empty destination; never a user project. -pub(super) fn materialize( - objects: &Objects, - snapshot: &WorkspaceSnapshot, - destination: &Path, -) -> Result<(), StoreError> { - let dir = Dir::open_ambient_dir(destination, cap_std::ambient_authority())?; - materialize_in(objects, snapshot, &dir) -} +/// The caller supplies an exclusively owned, empty destination. pub(super) fn materialize_in( objects: &Objects, snapshot: &WorkspaceSnapshot, diff --git a/plugins/agent/api/README.md b/plugins/agent/api/README.md index 9cc168a2746..0b1ed33714e 100644 --- a/plugins/agent/api/README.md +++ b/plugins/agent/api/README.md @@ -74,52 +74,54 @@ On macOS, active watches also compare direct-child metadata. Each scan is follow ## History checkpoint management -The [checkpoint contract](src/contract/checkpoints.rs) exposes explicit, history-only management through the existing Agent endpoint: +The [checkpoint contract](src/contract/checkpoints.rs) exposes explicit checkpoint management through the existing Agent endpoint: | Method | Mode | Result | | --- | --- | --- | | `previewHistoryCheckpoint` | Query | Session closure membership and live revisions | -| `publishHistoryCheckpoint` | Mutation | Durable publication state and cleanup status | -| `historyCheckpointReceipt` | Query | Original Host observation and domain publication receipt | +| `publishCheckpoint` | Mutation | Durable publication state and cleanup status | +| `checkpointReceipt` | Query | Original Host observation and domain publication receipt | | `historyCheckpointHead` | Query | Current checkpoint head, or null | | `readHistoryCheckpoint` | Subscription | Exact retained native history bundle | Preview the source, then publish with its `confirmedSubtreeDigest` and an explicit `expectedRevision` (null only for the first head). The digest confirms membership, not an earlier content revision; the manifest records the actual consistent cut. Busy sources fail instead of queuing. Live messages do not advance the checkpoint head. Publication uses ordinary Host mutation preparation/confirmation; retain the original `RecoveryRef` **and input** before confirmation. Never replace an uncertain operation with a new ID. A CAS conflict is terminal for that operation; a newly chosen expectation requires a new operation. -`historyCheckpointReceipt` requires both endpoint access and original-operation recovery authorization. It performs a read-only lookup, including after Root reopen, and never recaptures current history. `publication.state.kind = published` confirms the checkpoint even if `operation.outcome` remains `unresolved` and its cleanup remains `unconfirmed`: these are separate transactions. The query does not forge Host completion or release original-operation custody. Null means no matching retained fact was found, not proof that execution cannot occur. `interrupted` identifies capture that could not complete; `prepared` is awaiting Root recovery, and `unavailable` needs investigation rather than recapture under the old operation. Publication cleanup failure does not undo an already committed head. +`checkpointReceipt` requires both endpoint access and original-operation recovery authorization. It performs a read-only lookup, including after Root reopen, and never recaptures current history. `publication.state.kind = published` confirms the checkpoint even if `operation.outcome` remains `unresolved` and its cleanup remains `unconfirmed`: these are separate transactions. The query does not forge Host completion or release original-operation custody. Null means no matching retained fact was found, not proof that execution cannot occur. `interrupted` identifies capture that could not complete; `prepared` is awaiting Root recovery, and `unavailable` needs investigation rather than recapture under the old operation. Publication cleanup failure does not undo an already committed head. -For reads, first obtain a head, then request its exact revision. A moved head fails rather than selecting latest; old success receipts do not grant reads of old revisions. An admitted read retains its verified file and may finish if the head moves afterward. The subscription emits `native_history_v1` metadata, contiguous Base64 `chunk` frames (at most 32 KiB decoded), then `end` with the complete-object SHA-256 and byte length. The payload is native `MAKA-SESSION` v2, not tar.gz or the former TypeScript checkpoint format. Require matching metadata, contiguous offsets, digest, End, successful null completion and cleanup before accepting a download. Missing End is incomplete delivery. Backpressure bounds queued chunks; cancellation waits for the original reader to drain. No caller-provided file path is accepted. +For reads, first obtain a head, then request its exact revision. A moved head fails rather than selecting latest; old success receipts do not grant reads of old revisions. An admitted read retains its verified file and may finish if the head moves afterward. The subscription emits `checkpoint` metadata (with an optional Workspace reference), contiguous Base64 `chunk` frames (at most 32 KiB decoded), then `end` with the complete-object SHA-256 and byte length. The payload is native `MAKA-SESSION` v2, not tar.gz or the former TypeScript checkpoint format. Require matching metadata, contiguous offsets, digest, End, successful null completion and cleanup before accepting a download. Missing End is incomplete delivery. Backpressure bounds queued chunks; cancellation waits for the original reader to drain. No caller-provided file path is accepted. -These are management methods, not model tools. Scope comes from the admitted Agent assembly; model-origin calls and foreign namespaces are rejected. `publishHistoryCheckpoint` includes only the native bundle's historical closure and referenced artifacts. Workspace and bounded recovery use the explicit methods below; neither transfers all Memory/Goal state or cross-Host execution authority. Publication currently requires the Root store's Unix implementation; unsupported platforms fail explicitly. Private immutable objects are conservatively retained; automatic triggers, UI and retention/GC remain separate work. Workspace publication and bounded local recovery use the explicit methods below. +These are management methods, not model tools. Scope comes from the admitted Agent assembly; model-origin calls and foreign namespaces are rejected. With `workspaceConsistency: null`, `publishCheckpoint` includes only the native bundle's historical closure and referenced artifacts. Workspace and bounded recovery use the explicit methods below; neither transfers all Memory/Goal state or cross-Host execution authority. Publication currently requires the Root store's Unix implementation; unsupported platforms fail explicitly. Private immutable objects are conservatively retained; automatic triggers, UI and retention/GC remain separate work. Workspace publication and bounded local recovery use the explicit methods below. ### Exact read-only loading Call `loadHistoryCheckpoint` as a prepared Host mutation with `{ sessionId, revision }` from `historyCheckpointHead`. Retain its original `RecoveryRef` and input. The current retained revision is verified and claimed before destination writes; a moved Head rejects a new load. Once claimed, recovery finishes from the pinned manifest and bundle even if Head advances. The `ready` result contains an `inspection` locator and the imported `sessionIds`. -The inspection preserves historical Session IDs in a separate private database. It does not overwrite live Sessions or register runnable copies in the ordinary catalog. No model, tool, pending-message consumer or background Agent task is started by loading. Source configuration is historical evidence, never a destination execution grant. For `native_session_v1`, loading also materializes the recorded workspace in private inspection storage and returns its consistency metadata. It never rebinds the live Session, starts execution, or imports into another Host. +The inspection preserves historical Session IDs in a separate private database. It does not overwrite live Sessions or register runnable copies in the ordinary catalog. No model, tool, pending-message consumer or background Agent task is started by loading. Source configuration is historical evidence, never a destination execution grant. When a Workspace is present, loading verifies its inventory and immutable objects and returns its consistency metadata; it does not copy the files into inspection storage. It never rebinds the live Session, starts execution, or imports into another Host. Use `historyCheckpointLoadReceipt` with `{ reference, input }` after an uncertain response. It observes the original Host operation and domain load independently, without starting another load. `preparing` awaits recovery from the original materials; `unavailable` requires investigation. A domain `ready` does not imply Host outcome/cleanup confirmation. Root startup resumes pending imports before serving inspections; an existing native import receipt reconciles a committed destination whose load receipt was lost. -These queries take the returned `inspection` and a member `sessionId`: +These reads take the returned `inspection` and a member `sessionId`: | Method | Additional input | Result | | --- | --- | --- | | `loadedHistoryCheckpointHistory` | `position`, optional `includeThinking` | Existing bounded `HistoryPage`, including tool-result text | | `loadedHistoryCheckpointArtifacts` | `offset` | At most 32 artifact descriptors and `nextOffset` | -| `loadedHistoryCheckpointArtifact` | `artifactId`, byte `offset` | Metadata and at most 32 KiB Base64 payload; null if not found | +| `loadedHistoryCheckpointArtifact` | `artifactId`, byte `offset` | Subscription: contiguous chunks of at most 32 KiB with metadata; null if not found | -Use returned history cursors and artifact offsets to page without reading the whole checkpoint into memory. An inspection is retained independently of later Head changes. Inspection databases and immutable objects currently have no deletion/GC API; repeated **new** loads consume additional disk. Retrying the same original operation reuses its destination. These methods require the same management authorization and namespace as publication; locators are not bearer capabilities. +Use history cursors and artifact-list offsets for pagination. Each artifact subscription uses one read-only database connection for the entire transfer, with backpressure and cancellation; require successful completion and cleanup. Inspection reads do not acquire a writer lease, run migrations or sync directories, and can run concurrently. An inspection is retained independently of later Head changes. Inspection databases and immutable objects currently have no deletion/GC API; repeated **new** loads consume additional disk. Retrying the same original operation reuses its destination. These methods require the same management authorization and namespace as publication; locators are not bearer capabilities. ### Workspace publication and local continuation -`publishSessionCheckpoint` is a Host mutation with `{ history: { sessionId, expectedRevision, confirmedSubtreeDigest }, workspaceConsistency }`; `sessionCheckpointReceipt` takes its original `{ reference, input }`. The ordinary native Session must be idle and have a single Workspace. The first implementation rejects publication/restoration while this native Owner has active work and holds its admission gate during copying; large projects can delay new messages. The source path comes from its validated Host binding. This produces `native_session_v1`: the native history bundle plus an immutable workspace inventory and content-addressed file objects. The history subscription still streams only the bundle; its first frame identifies the variant and workspace inventory. +`publishCheckpoint` takes `{ sessionId, expectedRevision, confirmedSubtreeDigest, workspaceConsistency }`; `checkpointReceipt` takes its original `{ reference, input }`. One API and manifest represent history-only (`workspaceConsistency: null`) and Workspace publication. With Workspace enabled, the ordinary native Session must be idle and have a single Workspace. Admission excludes that Session, overlapping Workspace execution, and its in-flight Goal producers. Global run/goal locks are not held during copying, so unrelated Sessions can continue. The source path comes from its validated Host binding. The manifest contains the native history bundle, its event-history cut, and an optional immutable Workspace inventory with content-addressed files. The history subscription streams the bundle and identifies any Workspace in the same `checkpoint` header. Publication/receipt output has `workspace: null` or `{ consistency, warning }`. -`workspaceConsistency` is required and is either `caller_confirmed_quiescent` or `unverified`. **Neither mode fences external writers or requires Apple entitlements.** Display `external_writers_not_fenced` to the user: keep editors, terminals, other agents and background writers stopped throughout capture. Caller confirmation is an assertion, not an OS snapshot proof. Without that assurance the result is explicitly unverified and must not be presented as a consistent/trusted checkpoint. Detected source changes abort publication in both modes; a successful hash check proves the saved bytes' integrity, not that files came from one point in time. The mode and warning survive manifest storage, loading and restoration. +For Workspace capture, `workspaceConsistency` is either `caller_confirmed_quiescent` or `unverified`. **Neither mode fences external writers or requires Apple entitlements.** Display `external_writers_not_fenced` to the user: keep editors, terminals, other agents and background writers stopped throughout capture. Caller confirmation is an assertion, not an OS snapshot proof. Without that assurance the result is explicitly unverified and must not be presented as a consistent/trusted checkpoint. Detected source changes abort publication in both modes; a successful hash check proves the saved bytes' integrity, not that files came from one point in time. The mode and warning survive manifest storage, loading and restoration. -The v1 file policy excludes `.git`, `node_modules`, `.DS_Store`, `__pycache__`, `.cache`, and `.maka-workspace.json`. Git metadata, dependencies and the old local workspace marker are not portable task state in this format. Known credential filenames (including real `.env` files, private keys and credential directories), symlinks and special files are rejected; filename checks are not a universal secret scanner. Limits are 20,000 entries, 2 GiB total file content, 64 directory levels and a 16 MiB inventory. Regular files, executable bits and empty directories are preserved. Unsupported/nonportable paths fail instead of being rewritten. +Capture honors project `.gitignore`, nested ignore files and `.git/info/exclude`, without global or ancestor ignore configuration. It additionally excludes `.git`, `node_modules`, `.DS_Store`, `__pycache__`, `.cache`, `.maka-workspace.json` and known credential filenames/directories (including real `.env` files). Excluded paths are omitted, not whole-capture errors. Ordinary `.pem` and `.key` fixtures are allowed; filenames are not a universal secret scanner. Selected symlinks, special files and nonportable paths fail explicitly. Regular files, executable bits and empty directories are preserved. There is no fixed file-count, byte-size or elapsed-time gate; disk costs grow with included data, and memory with inventory size. Large captures still incur I/O and retain their Session admission until physical completion. -`restoreSessionCheckpoint` takes `{ checkpoint: { sessionId, revision }, parent: DirectorySelection, permissions, backend, acceptExternalWritersRisk: true }`. Select an allowed parent directory; the Host creates a **new** `maka-restored-…` child and never overwrites the original project. Only an ordinary native Session in the **same Root**, still at the captured live revision, is supported. Advanced history, moved checkpoint Head, WorkHub, child Sessions, Goal state and executor backends are rejected. The target model and permissions are explicit; old grants are retired. An unverified source remains unverified after restoration. +The API and private JSON are development contracts: no legacy publication endpoints, manifest variants or implicit missing-Workspace defaults are retained. Existing development checkpoint data in the former shape requires explicit offline migration or recreation. + +`restoreSessionCheckpoint` takes `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`. Supply an absolute path under an allowed directory root; physical identity is captured and rechecked by the Host, not supplied by the caller. the Host creates a **new** `maka-restored-…` child and never overwrites the original project. Only an ordinary native Session in the **same Root**, still at the captured event-history cut, is supported. Read markers, titles, pins and prior restores do not invalidate unchanged history; the current metadata revision is used only for the restore commit CAS. Advanced history, moved checkpoint Head, WorkHub, child Sessions, Goal state and executor backends are rejected. The target model and permissions are explicit; old grants are retired. An unverified source remains unverified after restoration. The restore atomically commits the new workspace binding and original restore receipt. It keeps the Session ID and existing checkpoint Head; a later publication uses that Head's revision as its CAS expectation. It does **not** roll back history or reconstruct a lost Root. Loading/rebinding does not execute a model or tool. Send an ordinary new message explicitly to continue; it uses the new Workspace. Cross-Root/Host authority handoff, arbitrary historical rollback, full auxiliary-state migration and mid-turn recovery are outside this implementation. -After an uncertain restore response, use `sessionCheckpointRestoreReceipt` with the original `{ reference, input }`. Querying a committed receipt does not require the old destination path still to exist. A durable request alone does not own its destination: only a successfully copied and synced directory has its physical identity recorded. Retry and commit require that same directory, even if a replacement has identical contents. Before commit, occupied, altered or incomplete destinations fail closed and never rebind the Session; retain them for inspection and use a newly selected operation/destination after resolving the failed preparation. Restore also excludes in-flight Goal producers until the binding commits. No automatic deletion of user-visible recovery directories is performed. Private inspection databases, workspace copies and immutable file objects consume retained disk; automatic GC is not implemented. +After an uncertain restore response, use `sessionCheckpointRestoreReceipt` with the original `{ reference, input }`. Querying a committed receipt does not require the old destination path still to exist. A durable request alone does not own its destination: only a successfully copied and synced directory has its physical identity recorded. Retry and commit require that same directory, even if a replacement has identical contents. Before commit, occupied, altered or incomplete destinations fail closed and never rebind the Session; retain them for inspection and use a newly selected operation/destination after resolving the failed preparation. Restore also excludes in-flight Goal producers until the binding commits. No automatic deletion of user-visible recovery directories is performed. Private inspection databases, restored directories and immutable file objects consume retained disk; automatic GC is not implemented. diff --git a/plugins/agent/api/README.zh-CN.md b/plugins/agent/api/README.zh-CN.md index 74d13c3725e..32fe0c0f983 100644 --- a/plugins/agent/api/README.zh-CN.md +++ b/plugins/agent/api/README.zh-CN.md @@ -74,54 +74,56 @@ macOS 上,活动订阅还会比较直接子项的元数据。每次扫描后 ## 历史 checkpoint 管理 -[Checkpoint 契约](src/contract/checkpoints.rs)通过现有 Agent endpoint 提供显式的 history-only 管理: +[Checkpoint 契约](src/contract/checkpoints.rs)通过现有 Agent endpoint 提供显式的 checkpoint 管理: | 方法 | 模式 | 返回内容 | | --- | --- | --- | | `previewHistoryCheckpoint` | Query | Session 历史闭包成员及实时 revision | -| `publishHistoryCheckpoint` | Mutation | 持久发布状态及清理状态 | -| `historyCheckpointReceipt` | Query | 原 Host 操作观测及领域发布回执 | +| `publishCheckpoint` | Mutation | 持久发布状态及清理状态 | +| `checkpointReceipt` | Query | 原 Host 操作观测及领域发布回执 | | `historyCheckpointHead` | Query | 当前 checkpoint Head,或 null | | `readHistoryCheckpoint` | Subscription | 指定 retained revision 的原生历史包 | 先预览,再携带 `confirmedSubtreeDigest` 和显式 `expectedRevision` 发布;只有创建第一个 Head 时预期为 null。摘要确认成员集合,不固定此前的内容 revision;Manifest 记录实际一致捕获的切点。繁忙源直接失败,不进入等待队列。日常消息不推进 checkpoint Head。发布经过普通 Host mutation 的准备与确认,确认前保存原 `RecoveryRef` **及原始输入**。结果未知时查询原操作,不换 ID 重发。CAS 冲突对原操作是终态,修改期望需要新操作。 -`historyCheckpointReceipt` 同时要求 endpoint 访问和原操作恢复权限。包括 Root 重开后的查询都只读原事实,不重新捕获当前历史。`publication.state.kind = published` 确认存档成功,即使 `operation.outcome` 仍为 `unresolved`、清理仍为 `unconfirmed`:二者来自不同事务。查询不会伪造 Host 完成,也不释放原操作的工作持有权。null 只表示未找到对应保留事实,不证明操作不可能执行。`interrupted` 表示捕获未完成,`prepared` 等待 Root 恢复,`unavailable` 需要排查,不能用原操作重新捕获。发布后的清理失败不撤销已提交 Head。 +`checkpointReceipt` 同时要求 endpoint 访问和原操作恢复权限。包括 Root 重开后的查询都只读原事实,不重新捕获当前历史。`publication.state.kind = published` 确认存档成功,即使 `operation.outcome` 仍为 `unresolved`、清理仍为 `unconfirmed`:二者来自不同事务。查询不会伪造 Host 完成,也不释放原操作的工作持有权。null 只表示未找到对应保留事实,不证明操作不可能执行。`interrupted` 表示捕获未完成,`prepared` 等待 Root 恢复,`unavailable` 需要排查,不能用原操作重新捕获。发布后的清理失败不撤销已提交 Head。 -读取时先获取 Head,再指定其精确 revision;Head 已移动则失败,不自动选择最新。历史成功回执不授予旧 revision 读取能力。已经受理的读取持有校验后的文件,随后 Head 移动仍可完成。流依次返回 `native_history_v1` 元数据、连续偏移的 Base64 `chunk`(解码后每块最多 32 KiB)及含完整对象 SHA-256、字节数的 `end`。内容是原生 `MAKA-SESSION` v2,不是 tar.gz,也不兼容旧 TypeScript checkpoint 格式。接收方须检查元数据、连续偏移、摘要、End、成功的 null 完成及清理结果;缺少 End 就是不完整传输。背压限制排队数据块,取消等待原读取工作排空,不接受调用方任意文件路径。 +读取时先获取 Head,再指定其精确 revision;Head 已移动则失败,不自动选择最新。历史成功回执不授予旧 revision 读取能力。已经受理的读取持有校验后的文件,随后 Head 移动仍可完成。流依次返回 `checkpoint` 元数据(带可空的 Workspace 引用)、连续偏移的 Base64 `chunk`(解码后每块最多 32 KiB)及含完整对象 SHA-256、字节数的 `end`。内容是原生 `MAKA-SESSION` v2,不是 tar.gz,也不兼容旧 TypeScript checkpoint 格式。接收方须检查元数据、连续偏移、摘要、End、成功的 null 完成及清理结果;缺少 End 就是不完整传输。背压限制排队数据块,取消等待原读取工作排空,不接受调用方任意文件路径。 -这些方法属于管理入口,不是模型工具。Scope 来自已获准的 Agent 装配;模型来源调用和其他 namespace 被拒绝。`publishHistoryCheckpoint` 只包含原生 Bundle 的历史闭包及关联 Artifact;Workspace 和受限恢复使用下文的显式方法。两者均不包含完整 Memory/Goal 状态或跨 Host 执行授权。发布目前依赖 Root 存储的 Unix 实现,不支持的平台显式失败。不可变对象保守保留,自动触发、UI 和 retention/GC 另行推进。 +这些方法属于管理入口,不是模型工具。Scope 来自已获准的 Agent 装配;模型来源调用和其他 namespace 被拒绝。`workspaceConsistency: null` 时,`publishCheckpoint` 只包含原生 Bundle 的历史闭包及关联 Artifact;Workspace 和受限恢复使用下文的显式方法。两者均不包含完整 Memory/Goal 状态或跨 Host 执行授权。发布目前依赖 Root 存储的 Unix 实现,不支持的平台显式失败。不可变对象保守保留,自动触发、UI 和 retention/GC 另行推进。 ### 精确只读加载 从 `historyCheckpointHead` 取得 `{ sessionId, revision }`,通过 Host 的 mutation 准备与确认调用 `loadHistoryCheckpoint`,保存原 `RecoveryRef` 和输入。写入目标之前先校验并登记当前 retained revision;Head 已移动则拒绝新的加载。登记成功后,即使 Head 推进,恢复仍使用已固定的 Manifest 和 Bundle。`ready` 返回 `inspection` 定位信息及导入的 `sessionIds`。 -检查副本在独立私有数据库中保留历史 Session ID,不覆盖实时 Session,也不向普通目录注册可运行副本。加载不会启动模型、工具、待处理消息消费者或 Agent 后台任务。源配置只作为历史证据,不成为目标执行授权。对 `native_session_v1` 还会在私有检查区物化 Workspace,但不自动切换实时目录或继续执行,也不向另一 Host 导入。 +检查副本在独立私有数据库中保留历史 Session ID,不覆盖实时 Session,也不向普通目录注册可运行副本。加载不会启动模型、工具、待处理消息消费者或 Agent 后台任务。源配置只作为历史证据,不成为目标执行授权。若包含 Workspace,加载只验证清单及不可变文件对象并返回一致性信息,不在检查区复制文件、不切换实时目录或执行任务,也不向另一 Host 导入。 响应未知时,携带 `{ reference, input }` 查询 `historyCheckpointLoadReceipt`。它分别返回原 Host 操作观测与领域加载结果,不启动另一加载。`preparing` 等待从原材料恢复,`unavailable` 需要排查;领域 `ready` 不代表 Host 已确认结果或清理。Root 在开放检查接口前恢复未完成导入;若目标已提交而加载回执丢失,使用原生导入回执找回原结果。 -以下查询携带返回的 `inspection` 和其中一个成员 `sessionId`: +以下读取接口携带返回的 `inspection` 和其中一个成员 `sessionId`: | 方法 | 其他输入 | 结果 | | --- | --- | --- | | `loadedHistoryCheckpointHistory` | `position`,可选 `includeThinking` | 既有有界 `HistoryPage`,包含工具结果文本 | | `loadedHistoryCheckpointArtifacts` | `offset` | 最多 32 条 Artifact 元数据及 `nextOffset` | -| `loadedHistoryCheckpointArtifact` | `artifactId`、字节 `offset` | 元数据及最多 32 KiB 的 Base64 内容;不存在返回 null | +| `loadedHistoryCheckpointArtifact` | `artifactId`、字节 `offset` | 订阅:连续返回元数据及最多 32 KiB 的 Base64 块;不存在返回 null | 使用返回的历史游标和 Artifact 偏移分页,无需把整个 checkpoint 放入内存。检查副本独立于后续 Head 变化保留。目前检查数据库和不可变对象没有删除/GC API;不断发起**新**加载会增加磁盘占用,原操作重试复用原目标。这些方法与发布使用相同的管理授权和 namespace;定位信息本身不授予访问权。 ### Workspace 发布与本地继续执行 -`publishSessionCheckpoint` 是 Host mutation,输入 `{ history: { sessionId, expectedRevision, confirmedSubtreeDigest }, workspaceConsistency }`;`sessionCheckpointReceipt` 使用原始 `{ reference, input }` 查询回执。只支持空闲的普通 native Session 和单个 Workspace。首版在该 native Owner 存在活跃任务时拒绝发布/恢复,复制期间持有准入锁;大项目会延迟新消息,不能承诺无性能影响。路径来自 Host 验证过的 Session 绑定。产物 `native_session_v1` 包括历史 Bundle、不可变 Workspace 清单和按内容寻址的文件对象。历史订阅仍只传输 Bundle,首帧会标明类型及 Workspace 清单引用。 +`publishCheckpoint` 输入 `{ sessionId, expectedRevision, confirmedSubtreeDigest, workspaceConsistency }`,`checkpointReceipt` 使用原始 `{ reference, input }`。统一接口及 Manifest 表达纯历史(`workspaceConsistency: null`)和带 Workspace 两种模式。带 Workspace 时只支持空闲的普通 native Session 和单个 Workspace;准入排除目标 Session、共享或嵌套 Workspace 的执行以及该 Session 尚未完成的 Goal producer。复制期间不持有全 Agent 的 runs/goals 锁,无关 Session 可以继续。路径来自 Host 验证过的绑定。Manifest 包含原生历史 Bundle、事件历史切面以及可空的 Workspace 清单/内容寻址文件对象。历史订阅使用同一种 `checkpoint` 首帧并传输 Bundle。发布及回执结果的 `workspace` 为 null 或 `{ consistency, warning }`。 -必须显式选择 `caller_confirmed_quiescent`(调用方确认复制全程已停写)或 `unverified`(不能保证)。**两者都不隔离外部写入者,也不要求 Apple entitlement。** 客户端应显示 `external_writers_not_fenced` 提示:捕获期间需暂停编辑器、终端、其他 Agent 和后台程序的写入。用户确认只是前提声明,不是系统快照证明;无法保证时不得把产物宣称为一致、可信的 checkpoint。两种模式检测到变化都中止发布;摘要校验仅证明保存的字节完整,不能证明各文件来自同一时刻。此标记随 Manifest、加载和恢复保留。 +捕获 Workspace 时必须显式选择 `caller_confirmed_quiescent`(调用方确认复制全程已停写)或 `unverified`(不能保证)。**两者都不隔离外部写入者,也不要求 Apple entitlement。** 客户端应显示 `external_writers_not_fenced` 提示:捕获期间需暂停编辑器、终端、其他 Agent 和后台程序的写入。用户确认只是前提声明,不是系统快照证明;无法保证时不得把产物宣称为一致、可信的 checkpoint。两种模式检测到变化都中止发布;摘要校验仅证明保存的字节完整,不能证明各文件来自同一时刻。此标记随 Manifest、加载和恢复保留。 -v1 文件策略排除 `.git`、`node_modules`、`.DS_Store`、`__pycache__`、`.cache` 和 `.maka-workspace.json`;不携带 Git 元数据、依赖目录或旧本地 Workspace 标记。发现已知凭据文件名(真实 `.env`、私钥、凭据目录等)、符号链接或特殊文件则拒绝;这不是通用的秘密内容扫描器。上限为 20,000 项、文件正文合计 2 GiB、64 层目录、16 MiB 清单。保留普通文件、可执行位和空目录;不可移植路径显式拒绝。 +捕获遵循项目 `.gitignore`、嵌套忽略文件和 `.git/info/exclude`,不读取全局或项目上层的忽略配置。额外排除 `.git`、`node_modules`、`.DS_Store`、`__pycache__`、`.cache`、`.maka-workspace.json` 及已知凭据文件名/目录(包括真实 `.env`)。这些路径被省略,不再导致整个捕获报错;普通 `.pem`、`.key` 测试夹具允许保留,但文件名规则并非通用秘密扫描器。被选中的符号链接、特殊文件和不可移植路径仍显式拒绝。保留普通文件、可执行位和空目录;不设固定文件数量、容量或时间门槛。磁盘成本随纳入数据量增长,内存随清单规模增长;大型捕获仍有 I/O 成本,完成前保留目标 Session 准入。 -`loadHistoryCheckpoint` 对 `native_session_v1` 还会在 Root 私有检查区物化文件,并返回一致性元数据;不会自动替换实时 Session 的目录或开始执行。 +API 和私有 JSON 仍是开发中契约:不保留旧发布端点、Manifest 双轨或缺失 Workspace 字段的隐式兼容。旧形状的开发 checkpoint 数据需要显式离线迁移或重新生成。 -`restoreSessionCheckpoint` 输入为 `{ checkpoint: { sessionId, revision }, parent: DirectorySelection, permissions, backend, acceptExternalWritersRisk: true }`。Host 在获准的父目录中新建 `maka-restored-…` 子目录,不覆盖原项目。首版仅支持**同 Root、实时 revision 尚未超过存档点**的普通 native Session;历史已推进、Head 已移动、WorkHub、子 Session、Goal 状态和 executor backend 都拒绝。显式选择目标模型与权限,撤销旧授权;unverified 不会因恢复变成可信。 +`loadHistoryCheckpoint` 只验证 Workspace 对象并返回一致性元数据,不复制 Workspace。历史及 Artifact 列表按游标分页;`loadedHistoryCheckpointArtifact` 是订阅,从指定 offset 连续传输最多 32 KiB 的块,完整传输只打开一次只读数据库连接。读取不申请写租约、不执行迁移、不同步目录,可并发读取;调用方仍须确认订阅完成及清理结果。 + +`restoreSessionCheckpoint` 输入为 `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`。输入为获准根内的绝对路径,目录物理身份由 Host 获取并重新验证。Host 在父目录中新建 `maka-restored-…` 子目录,不覆盖原项目。首版仅支持**同 Root、事件历史仍与存档点相同**的普通 native Session;已读、改名、置顶、之前的恢复不会使未变化的历史失效,当前元数据 revision 仅用于提交 CAS。历史已推进、Head 已移动、WorkHub、子 Session、Goal 状态和 executor backend 都拒绝。显式选择目标模型与权限,撤销旧授权;unverified 不会因恢复变成可信。 恢复在同一事务提交目录绑定与原操作回执,保留 Session ID 和 checkpoint Head,下一次发布仍以原 Head revision 做 CAS。不会回滚新历史,也不重建已丢失的 Root;加载/重新绑定不会自动调用模型或工具,显式发送下一条普通消息后才从新目录执行。跨 Root/Host 权威交接、任意历史回滚、完整辅助状态迁移和轮次中间恢复不在本次范围。 -回执未知时,使用原始 `{ reference, input }` 调用 `sessionCheckpointRestoreReceipt`;已提交回执不依赖目标目录仍在原位置。登记恢复请求不代表拥有目标目录;只有复制完成且落盘成功后,才记录实际创建目录的物理身份。重试与提交都要求仍是该目录,即便替换目录内容完全相同也拒绝。提交前若目标已被占用、不完整或被改变,则保持原 Session 不变,保留目录供检查;处理失败准备后使用新的操作/目标目录。恢复还会排除未完成的 Goal 写入,并持续到目录绑定提交。不自动删除用户可见的恢复目录;检查数据库、Workspace 副本和文件对象均占用持久磁盘,尚无自动 GC。 +回执未知时,使用原始 `{ reference, input }` 调用 `sessionCheckpointRestoreReceipt`;已提交回执不依赖目标目录仍在原位置。登记恢复请求不代表拥有目标目录;只有复制完成且落盘成功后,才记录实际创建目录的物理身份。重试与提交都要求仍是该目录,即便替换目录内容完全相同也拒绝。提交前若目标已被占用、不完整或被改变,则保持原 Session 不变,保留目录供检查;处理失败准备后使用新的操作/目标目录。恢复还会排除未完成的 Goal 写入,并持续到目录绑定提交。不自动删除用户可见的恢复目录;检查数据库、恢复目录和文件对象均占用持久磁盘,尚无自动 GC。 diff --git a/plugins/agent/api/src/contract.rs b/plugins/agent/api/src/contract.rs index c881d5701c5..f61d7e8aca1 100644 --- a/plugins/agent/api/src/contract.rs +++ b/plugins/agent/api/src/contract.rs @@ -1890,12 +1890,12 @@ pub fn contract() -> Contract { method::(MethodMode::Query), ), ( - "publishHistoryCheckpoint".into(), - method::(MethodMode::Mutation), + "publishCheckpoint".into(), + method::(MethodMode::Mutation), ), ( - "historyCheckpointReceipt".into(), - method::(MethodMode::Query), + "checkpointReceipt".into(), + method::(MethodMode::Query), ), ( "historyCheckpointHead".into(), @@ -1908,12 +1908,10 @@ pub fn contract() -> Contract { ("loadHistoryCheckpoint".into(), method::(MethodMode::Mutation)), ("restoreSessionCheckpoint".into(), method::(MethodMode::Mutation)), ("sessionCheckpointRestoreReceipt".into(), method::(MethodMode::Query)), - ("publishSessionCheckpoint".into(), method::(MethodMode::Mutation)), - ("sessionCheckpointReceipt".into(), method::(MethodMode::Query)), ("historyCheckpointLoadReceipt".into(), method::(MethodMode::Query)), ("loadedHistoryCheckpointHistory".into(), method::(MethodMode::Query)), ("loadedHistoryCheckpointArtifacts".into(), method::(MethodMode::Query)), - ("loadedHistoryCheckpointArtifact".into(), method::>(MethodMode::Query)), + ("loadedHistoryCheckpointArtifact".into(), method::>(MethodMode::Subscription)), ( "fork".into(), method::(MethodMode::Mutation), diff --git a/plugins/agent/api/src/contract/checkpoints.rs b/plugins/agent/api/src/contract/checkpoints.rs index 14c5bd22c53..a450a8fabb6 100644 --- a/plugins/agent/api/src/contract/checkpoints.rs +++ b/plugins/agent/api/src/contract/checkpoints.rs @@ -33,22 +33,11 @@ pub enum CheckpointWorkspaceConsistency { Unverified, } -#[derive(Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentSessionCheckpointPublish")] -pub struct SessionCheckpointPublish { - pub history: HistoryCheckpointPublish, - /// Must be explicitly selected. Maka does not fence other applications. - pub workspace_consistency: CheckpointWorkspaceConsistency, -} - #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentSessionCheckpointPublication")] -pub struct SessionCheckpointPublication { - pub publication: HistoryCheckpointPublication, - pub workspace_consistency: CheckpointWorkspaceConsistency, - /// Stable warning, present even with a caller attestation. +#[schemars(rename = "AgentCheckpointWorkspaceNotice")] +pub struct CheckpointWorkspaceNotice { + pub consistency: CheckpointWorkspaceConsistency, pub warning: CheckpointWorkspaceWarning, } @@ -61,26 +50,10 @@ pub enum CheckpointWorkspaceWarning { ExternalWritersNotFenced, } -#[derive(Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentSessionCheckpointReceiptRead")] -pub struct SessionCheckpointReceiptRead { - pub reference: RecoveryRef, - pub input: SessionCheckpointPublish, -} - -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentSessionCheckpointReceipt")] -pub struct SessionCheckpointReceipt { - pub operation: Option, - pub checkpoint: Option, -} - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentHistoryCheckpointPublish")] -pub struct HistoryCheckpointPublish { +#[schemars(rename = "AgentCheckpointPublish")] +pub struct CheckpointPublish { #[schemars(length(min = 1))] pub session_id: String, #[schemars(with = "maka_plugin_core::wire_schema::RequiredNullable")] @@ -88,6 +61,12 @@ pub struct HistoryCheckpointPublish { /// Membership confirmation returned by previewHistoryCheckpoint, not a live revision. #[schemars(regex(pattern = "^[a-f0-9]{64}$"))] pub confirmed_subtree_digest: String, + /// Null captures history only. Otherwise the caller explicitly selects the + /// Workspace consistency guarantee; other applications are never fenced. + #[schemars( + with = "maka_plugin_core::wire_schema::RequiredNullable" + )] + pub workspace_consistency: Option, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -124,11 +103,12 @@ pub struct HistoryCheckpointSession { } #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentHistoryCheckpointPublication")] -pub struct HistoryCheckpointPublication { +#[schemars(rename = "AgentCheckpointPublication")] +pub struct CheckpointPublication { pub state: HistoryCheckpointState, /// Cleanup failure does not undo a successful publication. pub cleanup_pending: bool, + pub workspace: Option, } #[derive(Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] @@ -143,21 +123,21 @@ pub enum HistoryCheckpointState { } #[derive(Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentHistoryCheckpointReceiptRead")] -pub struct HistoryCheckpointReceiptRead { +#[schemars(rename = "AgentCheckpointReceiptRead")] +pub struct CheckpointReceiptRead { pub reference: RecoveryRef, /// The exact original input. Changed input cannot select another publication. - pub input: HistoryCheckpointPublish, + pub input: CheckpointPublish, } #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[schemars(rename = "AgentHistoryCheckpointReceipt")] -pub struct HistoryCheckpointReceipt { +#[schemars(rename = "AgentCheckpointReceipt")] +pub struct CheckpointReceipt { /// Original Host observation. Domain success never fabricates Host cleanup confirmation. pub operation: Option, /// Authoritative domain result even when the Host response was lost. /// Absence is not permission to recapture under a replacement operation. - pub publication: Option, + pub publication: Option, } #[derive(Deserialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -177,18 +157,12 @@ pub struct HistoryCheckpointRead { )] #[schemars(rename = "AgentHistoryCheckpointFrame")] pub enum HistoryCheckpointFrame { - /// The streamed payload is history; Workspace is a separate manifest component. - NativeSessionV1 { - head: HistoryCheckpointHead, - inventory: HistoryCheckpointInventory, - bundle: HistoryCheckpointObject, - workspace: CheckpointWorkspaceSnapshot, - }, - /// The following bytes use the MAKA-SESSION v2 history format. - NativeHistoryV1 { + /// MAKA-SESSION v2 history bytes follow; Workspace objects are separate. + Checkpoint { head: HistoryCheckpointHead, inventory: HistoryCheckpointInventory, bundle: HistoryCheckpointObject, + workspace: Option, }, Chunk { #[schemars(range(max = 9_007_199_254_740_991u64))] @@ -333,19 +307,19 @@ mod tests { let Interface::Service { methods } = crate::contract().interface else { unreachable!() }; - assert_eq!( - methods["publishHistoryCheckpoint"].mode, - MethodMode::Mutation - ); + assert_eq!(methods["publishCheckpoint"].mode, MethodMode::Mutation); assert_eq!( methods["readHistoryCheckpoint"].mode, MethodMode::Subscription ); - let publish = - jsonschema::validator_for(&methods["publishHistoryCheckpoint"].input).unwrap(); - let valid = json!({"sessionId":"source","expectedRevision":null,"confirmedSubtreeDigest":"a".repeat(64)}); + let publish = jsonschema::validator_for(&methods["publishCheckpoint"].input).unwrap(); + let valid = json!({"sessionId":"source","expectedRevision":null,"confirmedSubtreeDigest":"a".repeat(64),"workspaceConsistency":null}); assert!(publish.is_valid(&valid)); - for key in ["expectedRevision", "confirmedSubtreeDigest"] { + for key in [ + "expectedRevision", + "confirmedSubtreeDigest", + "workspaceConsistency", + ] { let mut absent = valid.clone(); absent.as_object_mut().unwrap().remove(key); assert!(!publish.is_valid(&absent)); @@ -376,7 +350,6 @@ mod tests { "historyCheckpointLoadReceipt", "loadedHistoryCheckpointHistory", "loadedHistoryCheckpointArtifacts", - "loadedHistoryCheckpointArtifact", ] { assert_eq!(methods[method].mode, MethodMode::Query); } @@ -395,6 +368,10 @@ mod tests { invalid[key] = value; assert!(!validator.is_valid(&invalid), "{key}"); } + assert_eq!( + methods["loadedHistoryCheckpointArtifact"].mode, + MethodMode::Subscription + ); let validator = jsonschema::validator_for(&methods["loadedHistoryCheckpointArtifact"].input).unwrap(); let mut valid = json!({"inspection":{"sourceSessionId":"source","operationId":Uuid::new_v4()},"sessionId":"source","artifactId":"a","offset":0}); @@ -411,7 +388,9 @@ mod tests { #[schemars(rename = "AgentSessionCheckpointRestore")] pub struct SessionCheckpointRestore { pub checkpoint: HistoryCheckpointRead, - pub parent: super::DirectorySelection, + /// Absolute parent path. The Host captures and validates its directory identity. + #[schemars(length(min = 1))] + pub parent: String, pub permissions: super::WorkspacePermissions, pub backend: super::BackendPreference, pub accept_external_writers_risk: bool, diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md index a376e3cc037..0ae166af514 100644 --- a/plugins/agent/native/README.md +++ b/plugins/agent/native/README.md @@ -21,7 +21,7 @@ [简体中文](README.zh-CN.md) -[`HistoryCheckpointPublisher`](src/checkpoints.rs) binds the admitted native Session +[`CheckpointPublisher`](src/checkpoints.rs) binds the admitted native Session namespace and original `RecoveryRef` to Root-owned history capture and recovery. The [service adapter](src/owner/checkpoints.rs) exposes explicit management methods with Host admission, original receipt authorization and bounded subscription reads. diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md index d4d558ab09f..de2df0cba57 100644 --- a/plugins/agent/native/README.zh-CN.md +++ b/plugins/agent/native/README.zh-CN.md @@ -21,7 +21,7 @@ [English](README.md) -[`HistoryCheckpointPublisher`](src/checkpoints.rs) 将已获准的原生 Session namespace +[`CheckpointPublisher`](src/checkpoints.rs) 将已获准的原生 Session namespace 和原 `RecoveryRef` 绑定到 Root 持有的历史捕获与恢复流程。 [服务适配层](src/owner/checkpoints.rs)通过 Host 准入、原操作回执授权和有界订阅读取提供显式管理方法。 领域回执可确认 checkpoint 成功,但不伪造通用 Host 的清理确认。 diff --git a/plugins/agent/native/src/checkpoints.rs b/plugins/agent/native/src/checkpoints.rs index 407e3eb9a5a..bf837303cde 100644 --- a/plugins/agent/native/src/checkpoints.rs +++ b/plugins/agent/native/src/checkpoints.rs @@ -28,7 +28,7 @@ use maka_plugin_core::operation::RecoveryRef; use std::sync::Arc; #[derive(Clone)] -pub struct HistoryCheckpointPublisher { +pub struct CheckpointPublisher { store: Arc, scope: Scope, prefix: String, @@ -42,7 +42,7 @@ pub struct Capture { pub workspace: Option, } -impl HistoryCheckpointPublisher { +impl CheckpointPublisher { /// Called by trusted native assembly, never with model-authored scope. pub fn new(store: Arc, scope: Scope, coordinator: bool) -> Self { let prefix = maka_agent_authority::session_namespace( @@ -79,12 +79,7 @@ impl HistoryCheckpointPublisher { || operation.source.deployment != self.scope.deployment || operation.namespace != self.scope.namespace || operation.export != maka_agent_api::EXPORT - || operation.method - != if capture.workspace.is_some() { - "publishSessionCheckpoint" - } else { - "publishHistoryCheckpoint" - } + || operation.method != "publishCheckpoint" { return Err(StoreError::InvalidTransition( "checkpoint operation belongs to another authority".into(), @@ -208,7 +203,7 @@ mod tests { deployment: Uuid::from_u128(1), namespace: InstanceId(vec![Uuid::from_u128(2)]), }; - let publisher = HistoryCheckpointPublisher::new(store.clone(), scope.clone(), false); + let publisher = CheckpointPublisher::new(store.clone(), scope.clone(), false); let id = format!("{}{}", publisher.prefix, "1".repeat(32)); store .create_session(&id, "create", &serde_json::json!({}), 1) @@ -228,7 +223,7 @@ mod tests { }, namespace: scope.namespace, export: maka_agent_api::EXPORT.into(), - method: "publishHistoryCheckpoint".into(), + method: "publishCheckpoint".into(), operation: Uuid::from_u128(3), fingerprint: format!("sha256-{}", "b".repeat(64)), contract: RecoveryContract { diff --git a/plugins/agent/native/src/owner.rs b/plugins/agent/native/src/owner.rs index 654b8317f22..e85fd2b8cec 100644 --- a/plugins/agent/native/src/owner.rs +++ b/plugins/agent/native/src/owner.rs @@ -173,6 +173,7 @@ struct Control { } #[derive(Default)] struct Runs { + checkpoints: Arc>>, closing: bool, controls: BTreeMap, preparing: BTreeMap, @@ -181,7 +182,9 @@ struct Runs { } impl Runs { fn busy(&self, session: &str) -> bool { - self.controls.contains_key(session) || self.preparing.contains_key(session) + self.controls.contains_key(session) + || self.preparing.contains_key(session) + || self.checkpoints.lock().unwrap().contains_key(session) } } struct CreationPolicy { @@ -734,13 +737,23 @@ impl Owner { if !self.owns(id) { return Err("agent_session_unavailable".into()); } + if self.runs.checkpointed(id) { + return Err("agent_checkpoint_busy".into()); + } let session = self .store .get_session::(id) .await .map_err(|_| "agent_store_unavailable".to_owned())? .ok_or("agent_session_unavailable")?; - resolve_session(session) + let session = resolve_session(session)?; + if !self.runs.workspace_available( + id, + std::path::Path::new(&session.configuration.workspace.path), + ) { + return Err("agent_checkpoint_busy".into()); + } + Ok(session) } pub(crate) fn caller_store( &self, @@ -977,7 +990,17 @@ impl Owner { }) } pub async fn mark_read(&self, input: MarkRead) -> Result { + if self.runs.checkpointed(&input.session_id) { + return Ok(MarkReadResult::Rejected { + reason: Rejection::Busy, + }); + } let runs = self.runs.lock().await; + if self.runs.checkpointed(&input.session_id) { + return Ok(MarkReadResult::Rejected { + reason: Rejection::Busy, + }); + } if runs.closing { return Ok(MarkReadResult::Rejected { reason: Rejection::Closed, @@ -1030,7 +1053,17 @@ impl Owner { if let Some(reason) = metadata_rejection(&input.title, &input.labels) { return Ok(MetadataResult::Rejected { reason }); } + if self.runs.checkpointed(&input.session_id) { + return Ok(MetadataResult::Rejected { + reason: Rejection::Busy, + }); + } let runs = self.runs.lock().await; + if self.runs.checkpointed(&input.session_id) { + return Ok(MetadataResult::Rejected { + reason: Rejection::Busy, + }); + } if runs.closing { return Ok(MetadataResult::Rejected { reason: Rejection::Closed, diff --git a/plugins/agent/native/src/owner/checkpoints.rs b/plugins/agent/native/src/owner/checkpoints.rs index 2d87d176de6..a5146d05534 100644 --- a/plugins/agent/native/src/owner/checkpoints.rs +++ b/plugins/agent/native/src/owner/checkpoints.rs @@ -22,7 +22,7 @@ use super::*; mod load; mod restore; mod session; -use crate::checkpoints::{Capture, HistoryCheckpointPublisher}; +use crate::checkpoints::{Capture, CheckpointPublisher}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use maka_capabilities::call::Scope; use maka_event_log::{StoreError, checkpoints as storage}; @@ -30,9 +30,90 @@ use maka_host_runtime::application::Context; use maka_plugin_core::operation::RecoveryRef; use tokio::io::AsyncReadExt; +pub(super) struct CheckpointAdmission { + _session: runs::Ordered, + _goal: runs::Ordered, + _workspace: runs::Checkpoint, +} + +impl Owner { + pub(super) async fn checkpoint_admission( + self: &Arc, + id: &str, + ) -> Result<(CheckpointAdmission, SessionRecord), String> { + let goal = self + .goals + .admissions + .try_ordered(id) + .map_err(|_| "agent_checkpoint_busy")?; + let ordered = self + .runs + .try_ordered(id) + .map_err(|_| "agent_checkpoint_busy")?; + let runs = self.runs.state().await; + if runs.closing || runs.busy(id) || runs.retained_cleanup.contains(id) { + return Err("agent_checkpoint_busy".into()); + } + let session = self.session(id).await?; + let path = std::path::PathBuf::from(&session.configuration.workspace.path); + let mut active = runs + .controls + .keys() + .chain(runs.preparing.keys()) + .chain(runs.retained_cleanup.iter()) + .cloned() + .chain(self.runs.ordered_sessions()) + .chain(self.goals.admissions.ordered_sessions()) + .filter(|active| active != id) + .collect::>(); + let mut after = None; + loop { + let pending = self + .store + .pending_message_sessions(&self.prefix, after.as_deref()) + .await + .map_err(|_| "agent_store_unavailable")?; + if pending.is_empty() { + break; + } + after = pending.last().cloned(); + active.extend(pending.into_iter().filter(|active| active != id)); + } + for active in active { + // Existing checkpoint reservations are compared atomically below. + if self.runs.checkpointed(&active) { + continue; + } + let other = resolve_session( + self.store + .get_session::(&active) + .await + .map_err(|_| "agent_store_unavailable")? + .ok_or("agent_session_unavailable")?, + )?; + if runs::overlaps( + &path, + std::path::Path::new(&other.configuration.workspace.path), + ) { + return Err("agent_checkpoint_busy".into()); + } + } + let workspace = self.runs.checkpoint(id, path)?; + drop(runs); + Ok(( + CheckpointAdmission { + _session: ordered, + _goal: goal, + _workspace: workspace, + }, + session, + )) + } +} + impl Owner { - fn history_checkpoints(&self) -> HistoryCheckpointPublisher { - HistoryCheckpointPublisher::new( + fn history_checkpoints(&self) -> CheckpointPublisher { + CheckpointPublisher::new( self.store.clone(), storage::Scope { state_domain: self.data_source.state_domain.clone(), @@ -68,27 +149,6 @@ impl Owner { .await } - pub async fn publish_history_checkpoint( - self: &Arc, - input: HistoryCheckpointPublish, - operation: &RecoveryRef, - scope: &Scope, - ) -> Result { - management(scope)?; - let operation = operation.clone(); - self.resource_work(scope, move |owner, _| { - Box::pin(async move { - owner - .history_checkpoints() - .publish(capture(input), &operation) - .await - .map(publication) - .map_err(failure) - }) - }) - .await - } - pub async fn history_checkpoint_head( self: &Arc, input: SessionRead, @@ -108,42 +168,6 @@ impl Owner { .await } - pub async fn history_checkpoint_receipt( - self: &Arc, - input: HistoryCheckpointReceiptRead, - context: Context, - ) -> Result { - management(context.scope())?; - let scope = context.scope().clone(); - self.resource_work(&scope, move |owner, _| { - Box::pin(async move { - let publisher = owner.history_checkpoints(); - let capture = capture(input.input); - // Validate domain identity before invoking the separately authorized Host lookup. - publisher - .validate_request(&capture, &input.reference) - .map_err(failure)?; - let operation = context.recover(input.reference.clone())?.await?; - let publication = if operation.is_some() { - publisher - .receipt(capture, &input.reference) - .await - .map_err(failure)? - .map(publication) - } else { - None - }; - // Host cleanup and a committed domain result are different facts. In - // particular a lookup must never manufacture an original DispatchClaim. - Ok(HistoryCheckpointReceipt { - operation, - publication, - }) - }) - }) - .await - } - pub async fn read_history_checkpoint( self: &Arc, input: HistoryCheckpointRead, @@ -163,34 +187,21 @@ impl Owner { .workspace() .cloned() .map(workspace_snapshot); - let (storage::Manifest::NativeHistory { - inventory: members, - bundle, - .. - } - | storage::Manifest::NativeSession { + let storage::Manifest { inventory: members, bundle, .. - }) = checkout.manifest; + } = checkout.manifest; let expected = bundle.clone(); emit( &context, &owner, &scope, - if let Some(workspace) = workspace { - HistoryCheckpointFrame::NativeSessionV1 { - head: head(checkout.head), - inventory: inventory(members), - bundle: object(bundle), - workspace, - } - } else { - HistoryCheckpointFrame::NativeHistoryV1 { - head: head(checkout.head), - inventory: inventory(members), - bundle: object(bundle), - } + HistoryCheckpointFrame::Checkpoint { + head: head(checkout.head), + inventory: inventory(members), + bundle: object(bundle), + workspace, }, ) .await?; @@ -262,7 +273,7 @@ async fn emit( .emit(serde_json::to_value(frame).map_err(|_| "agent_history_checkpoint_output_invalid")?) .await } -fn capture(input: HistoryCheckpointPublish) -> Capture { +fn capture(input: CheckpointPublish) -> Capture { Capture { session_id: input.session_id, expected_checkpoint_revision: input.expected_revision.map(storage::Revision), @@ -309,7 +320,7 @@ fn inventory(value: maka_event_log::bundle::Inventory) -> HistoryCheckpointInven .collect(), } } -fn publication(value: storage::Publication) -> HistoryCheckpointPublication { +fn publication(value: storage::Publication) -> CheckpointPublication { let state = match value.state { storage::PublicationState::Capturing => HistoryCheckpointState::Capturing, storage::PublicationState::Prepared { .. } => HistoryCheckpointState::Prepared, @@ -322,9 +333,10 @@ fn publication(value: storage::Publication) -> HistoryCheckpointPublication { storage::PublicationState::Interrupted => HistoryCheckpointState::Interrupted, storage::PublicationState::Unavailable { .. } => HistoryCheckpointState::Unavailable, }; - HistoryCheckpointPublication { + CheckpointPublication { state, cleanup_pending: value.cleanup_pending, + workspace: None, } } fn failure(error: StoreError) -> String { diff --git a/plugins/agent/native/src/owner/checkpoints/load.rs b/plugins/agent/native/src/owner/checkpoints/load.rs index dae314827ee..89431ad4258 100644 --- a/plugins/agent/native/src/owner/checkpoints/load.rs +++ b/plugins/agent/native/src/owner/checkpoints/load.rs @@ -150,29 +150,49 @@ impl Owner { pub async fn loaded_checkpoint_artifact( self: &Arc, input: CheckpointArtifactRead, - scope: &Scope, - ) -> Result, String> { - management(scope)?; - self.resource_work(scope, move |owner, _| { + context: Context, + ) -> Result { + management(context.scope())?; + let scope = context.scope().clone(); + let context = Arc::new(context); + self.resource_work(&scope.clone(), move |owner, _| { Box::pin(async move { - let record = owner + owner .history_checkpoints() .repository(&input.inspection.source_session_id) .await .map_err(failure)? - .loaded_artifact( + .stream_loaded_artifact( input.inspection.operation_id, input.session_id, input.artifact_id, input.offset, + move |record| { + let context = context.clone(); + let scope = scope.clone(); + let owner = owner.clone(); + async move { + if scope.cancellation.is_cancelled() || owner.stop.is_cancelled() { + return Err(maka_event_log::StoreError::InvalidTransition( + "checkpoint artifact cancelled".into(), + )); + } + let chunk = + record.map(|(record, offset, chunk)| CheckpointArtifactChunk { + artifact: artifact(record), + offset, + data: STANDARD.encode(chunk.bytes), + }); + context + .emit(serde_json::to_value(chunk)?) + .await + .map_err(maka_event_log::StoreError::InvalidTransition) + } + }, ) .await .map_err(failure)?; - Ok(record.map(|(record, chunk)| CheckpointArtifactChunk { - artifact: artifact(record), - offset: input.offset, - data: STANDARD.encode(chunk.bytes), - })) + Ok(Value::Null) }) }) .await diff --git a/plugins/agent/native/src/owner/checkpoints/restore.rs b/plugins/agent/native/src/owner/checkpoints/restore.rs index 53666dc9e80..bef6ac9451e 100644 --- a/plugins/agent/native/src/owner/checkpoints/restore.rs +++ b/plugins/agent/native/src/owner/checkpoints/restore.rs @@ -82,26 +82,9 @@ impl Owner { if !input.accept_external_writers_risk { return Err("agent_checkpoint_external_writers_not_fenced".into()); } - let runs = owner - .runs - .clone() - .try_lock_owned() - .map_err(|_| "agent_checkpoint_busy")?; - if runs.closing - || !runs.controls.is_empty() - || !runs.preparing.is_empty() - || !runs.retained_cleanup.is_empty() - { - return Err("agent_checkpoint_busy".into()); - } - // A producer admitted before the runs gate may not have saved - // its Goal yet. Do not wait here: producers also need runs. - let goal_producer = owner - .goals - .gate - .try_lock() - .map_err(|_| "agent_checkpoint_busy")?; - let session = owner.session(&input.checkpoint.session_id).await?; + let (admission, session) = owner + .checkpoint_admission(&input.checkpoint.session_id) + .await?; if owner.behavior.is_some() || session.archived || session.configuration.parent_session_id.is_some() @@ -125,9 +108,29 @@ impl Owner { return Err("agent_checkpoint_backend_unavailable".into()); } let root = owner.store.owned_root().map_err(failure)?; + let path = std::path::PathBuf::from(&input.parent); + if !path.is_absolute() { + return Err("agent_directory_unavailable".into()); + } + let path = tokio::task::spawn_blocking(move || path.canonicalize()) + .await + .map_err(|_| "agent_directory_unavailable")? + .map_err(|_| "agent_directory_unavailable")?; + if path.starts_with(root.control_directory()) + || path.starts_with(&owner.managed_worktrees) + { + return Err("agent_directory_unavailable".into()); + } + let selection = workspace::selection_at( + owner.workspace_roots.clone(), + path.to_string_lossy().into_owned(), + root.canonical_path().to_owned(), + ) + .await? + .ok_or("agent_directory_unavailable")?; let parent = workspace::observed( owner.workspace_roots.clone(), - input.parent.clone(), + selection, root.canonical_path().to_owned(), ) .await? @@ -137,7 +140,7 @@ impl Owner { fingerprint, revision: storage::Revision(input.checkpoint.revision), parent: parent.path.into(), - parent_identity: input.parent.directory_identity.clone(), + parent_identity: parent.selection.directory_identity, }; let prepared = repository .prepare_workspace_restore(request.clone()) @@ -190,8 +193,7 @@ impl Owner { .await .map(restored) .map_err(failure); - drop(goal_producer); - drop(runs); + drop(admission); result }) }) diff --git a/plugins/agent/native/src/owner/checkpoints/session.rs b/plugins/agent/native/src/owner/checkpoints/session.rs index d606a2ea14f..d008fa9fe62 100644 --- a/plugins/agent/native/src/owner/checkpoints/session.rs +++ b/plugins/agent/native/src/owner/checkpoints/session.rs @@ -30,31 +30,33 @@ fn consistency(value: CheckpointWorkspaceConsistency) -> storage::WorkspaceConsi } fn response( value: storage::Publication, - mode: CheckpointWorkspaceConsistency, -) -> SessionCheckpointPublication { - SessionCheckpointPublication { - publication: publication(value), - workspace_consistency: mode, + mode: Option, +) -> CheckpointPublication { + let mut result = publication(value); + result.workspace = mode.map(|consistency| CheckpointWorkspaceNotice { + consistency, warning: CheckpointWorkspaceWarning::ExternalWritersNotFenced, - } + }); + result } -fn requested(input: SessionCheckpointPublish) -> (Capture, CheckpointWorkspaceConsistency) { - let mut capture = capture(input.history); - capture.workspace = Some(storage::WorkspaceCapture { +fn requested(input: CheckpointPublish) -> (Capture, Option) { + let mode = input.workspace_consistency; + let mut capture = capture(input); + capture.workspace = mode.map(|mode| storage::WorkspaceCapture { root: Default::default(), identity: String::new(), - consistency: consistency(input.workspace_consistency), + consistency: consistency(mode), }); - (capture, input.workspace_consistency) + (capture, mode) } impl Owner { - pub async fn publish_session_checkpoint( + pub async fn publish_checkpoint( self: &Arc, - input: SessionCheckpointPublish, + input: CheckpointPublish, operation: &RecoveryRef, scope: &Scope, - ) -> Result { + ) -> Result { management(scope)?; let operation = operation.clone(); self.resource_work(scope, move |owner, _| { @@ -77,21 +79,14 @@ impl Owner { .map(|p| response(p, mode)) .map_err(failure); } - // Keep native admission closed until both history and workspace have been copied. - // Foreign Hosts and external applications remain covered only by the caller's assertion. - let runs = owner - .runs - .clone() - .try_lock_owned() - .map_err(|_| "agent_checkpoint_busy")?; - if runs.closing - || !runs.controls.is_empty() - || !runs.preparing.is_empty() - || !runs.retained_cleanup.is_empty() - { - return Err("agent_checkpoint_busy".into()); - } - let session = owner.session(&capture.session_id).await?; + let Some(workspace_mode) = mode else { + return publisher + .publish(capture, &operation) + .await + .map(|value| response(value, None)) + .map_err(failure); + }; + let (admission, session) = owner.checkpoint_admission(&capture.session_id).await?; if owner.behavior.is_some() || session.configuration.parent_session_id.is_some() || session @@ -120,36 +115,39 @@ impl Owner { capture.workspace = Some(storage::WorkspaceCapture { root: session.configuration.workspace.path.into(), identity: session.configuration.workspace.identity.into(), - consistency: consistency(mode), + consistency: consistency(workspace_mode), }); let result = publisher .publish(capture, &operation) .await .map(|p| response(p, mode)) .map_err(failure); - drop(runs); + drop(admission); result }) }) .await } - pub async fn session_checkpoint_receipt( + pub async fn checkpoint_receipt( self: &Arc, - input: SessionCheckpointReceiptRead, + input: CheckpointReceiptRead, context: Context, - ) -> Result { + ) -> Result { management(context.scope())?; let scope = context.scope().clone(); self.resource_work(&scope, move |owner, _| { Box::pin(async move { let publisher = owner.history_checkpoints(); let (capture, mode) = requested(input.input); + publisher + .validate_request(&capture, &input.reference) + .map_err(failure)?; let request = publisher .request(&capture, &input.reference) .map_err(failure)?; let operation = context.recover(input.reference)?.await?; - let checkpoint = if operation.is_some() { + let publication = if operation.is_some() { let repository = publisher .repository(&capture.session_id) .await @@ -170,9 +168,9 @@ impl Owner { } else { None }; - Ok(SessionCheckpointReceipt { + Ok(CheckpointReceipt { operation, - checkpoint, + publication, }) }) }) diff --git a/plugins/agent/native/src/owner/goal.rs b/plugins/agent/native/src/owner/goal.rs index 41159c7842b..d5f04222e46 100644 --- a/plugins/agent/native/src/owner/goal.rs +++ b/plugins/agent/native/src/owner/goal.rs @@ -31,6 +31,7 @@ use std::sync::{ use tokio::sync::Notify; pub(super) struct Goals { + pub(super) admissions: Arc, storage: Arc, pub(super) repository: Repository, pub(super) gate: Mutex<()>, @@ -50,6 +51,7 @@ impl Goals { .expect("goal namespace"), )); Self { + admissions: Arc::new(runs::Registry::default()), repository: Repository::new(storage.clone()), storage, gate: Mutex::new(()), @@ -312,6 +314,7 @@ impl Owner { session_id } }; + let admission = self.goals.admissions.ordered(session).await; self.session(session).await?; let runs = self.runs.lock().await; if runs.closing || self.stop.is_cancelled() { @@ -319,6 +322,7 @@ impl Owner { } let owner = self.clone(); let job = self.workers.spawn(async move { + let _admission = admission; let _gate = owner.goals.gate.lock().await; if let Some(result) = owner.goals.receipt(&operation).await? { return Ok(result); diff --git a/plugins/agent/native/src/owner/projects.rs b/plugins/agent/native/src/owner/projects.rs index a7dc790ba96..3bc9c9561be 100644 --- a/plugins/agent/native/src/owner/projects.rs +++ b/plugins/agent/native/src/owner/projects.rs @@ -432,6 +432,18 @@ impl Owner { if other.as_ref().map(reference) != destination { return Ok(ProjectResult::Changed); } + if current + .locations + .iter() + .chain(other.iter().flat_map(|p| &p.locations)) + .any(|location| { + !self + .runs + .workspace_available("", std::path::Path::new(&location.path)) + }) + { + return Ok(ProjectResult::Busy); + } let ids = std::iter::once(current.id.clone()) .chain(current.aliases.iter().cloned()) .chain(other.iter().flat_map(|record| { diff --git a/plugins/agent/native/src/owner/runs.rs b/plugins/agent/native/src/owner/runs.rs index f21a9e26694..ba8d1ff37dd 100644 --- a/plugins/agent/native/src/owner/runs.rs +++ b/plugins/agent/native/src/owner/runs.rs @@ -21,6 +21,7 @@ use super::Runs; use std::{ collections::BTreeMap, ops::{Deref, DerefMut}, + path::{Path, PathBuf}, sync::{Arc, Mutex as MapMutex, Weak}, }; use tokio::sync::{ @@ -30,14 +31,75 @@ use tokio::sync::{ /// Lifecycle and control mutations exclude submissions. Submissions to distinct /// Sessions share that lifecycle boundary, but never their per-Session ordering. -#[derive(Default)] pub(super) struct Registry { activity: Arc>, state: Arc>, sessions: MapMutex>>, + checkpoints: Arc>>, +} + +impl Default for Registry { + fn default() -> Self { + let checkpoints = Arc::new(MapMutex::new(BTreeMap::new())); + Self { + activity: Default::default(), + state: Arc::new(Mutex::new(Runs { + checkpoints: checkpoints.clone(), + ..Runs::default() + })), + sessions: Default::default(), + checkpoints, + } + } +} + +pub(super) fn overlaps(a: &Path, b: &Path) -> bool { + a.starts_with(b) || b.starts_with(a) +} + +pub(super) struct Checkpoint { + checkpoints: Arc>>, + session: String, +} +impl Drop for Checkpoint { + fn drop(&mut self) { + self.checkpoints.lock().unwrap().remove(&self.session); + } } impl Registry { + pub(super) fn ordered_sessions(&self) -> Vec { + self.sessions + .lock() + .unwrap() + .iter() + .filter(|(_, gate)| gate.strong_count() != 0) + .map(|(id, _)| id.clone()) + .collect() + } + pub(super) fn checkpointed(&self, session: &str) -> bool { + self.checkpoints.lock().unwrap().contains_key(session) + } + pub(super) fn workspace_available(&self, session: &str, path: &Path) -> bool { + self.checkpoints + .lock() + .unwrap() + .iter() + .all(|(id, active)| id != session && !overlaps(path, active)) + } + pub(super) fn checkpoint(&self, session: &str, path: PathBuf) -> Result { + let mut checkpoints = self.checkpoints.lock().unwrap(); + if checkpoints.contains_key(session) + || checkpoints.values().any(|active| overlaps(&path, active)) + { + return Err("agent_checkpoint_busy".into()); + } + checkpoints.insert(session.to_owned(), path); + Ok(Checkpoint { + checkpoints: self.checkpoints.clone(), + session: session.to_owned(), + }) + } pub(super) async fn state(&self) -> MutexGuard<'_, Runs> { self.state.lock().await } diff --git a/plugins/agent/native/src/owner/tests/checkpoints.rs b/plugins/agent/native/src/owner/tests/checkpoints.rs index e81ee3a06c9..b0f6b31093f 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints.rs @@ -56,14 +56,14 @@ async fn input(f: &Fixture, session: &str, expected: Value) -> Value { .await .unwrap(); assert_eq!(preview["rootSessionId"], session); - json!({"sessionId":session,"expectedRevision":expected,"confirmedSubtreeDigest":preview["subtreeDigest"]}) + json!({"sessionId":session,"expectedRevision":expected,"confirmedSubtreeDigest":preview["subtreeDigest"],"workspaceConsistency":null}) } async fn publish(f: &Fixture, input: Value, operation: Uuid) -> (RecoveryRef, Receipt) { let client = f.app.client(); let prepared = client .prepare_mutation( f.binding(2).await, - "publishHistoryCheckpoint".into(), + "publishCheckpoint".into(), input, scope(&f.issuer, true), operation, @@ -108,7 +108,7 @@ async fn read( let mut ended = false; while let Some(frame) = stream.recv().await { match frame["kind"].as_str().unwrap() { - "native_history_v1" => { + "checkpoint" => { assert!(begin.is_null()); begin = frame; } @@ -194,7 +194,7 @@ async fn host_publication_preserves_original_bytes_after_live_history_and_head_m turn(&f, &listener, &session, "after checkpoint").await; let observation = f .query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":reference,"input":original}), ) .await @@ -232,7 +232,7 @@ async fn host_publication_preserves_original_bytes_after_live_history_and_head_m ); let receipt = f .query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":reference,"input":original}), ) .await @@ -257,7 +257,7 @@ async fn busy_source_is_not_queued_or_republished_by_receipt_lookup() { let (reference,receipt)=publish(&f,original.clone(),operation).await; assert!(matches!(receipt,Receipt::Recorded {outcome:OperationOutcome::Unknown {ref diagnostic},..} if diagnostic=="agent_history_checkpoint_busy")); response(&mut socket).await; f.drained().await; - let receipt=f.query("historyCheckpointReceipt",json!({"reference":reference,"input":original})).await.unwrap(); + let receipt=f.query("checkpointReceipt",json!({"reference":reference,"input":original})).await.unwrap(); assert_eq!(receipt["publication"]["state"]["kind"],"interrupted"); assert!(f.query("historyCheckpointHead",json!({"sessionId":session})).await.unwrap().is_null()); let current=input(&f,&session,Value::Null).await; @@ -276,7 +276,7 @@ async fn host_policy_identity_original_fingerprint_and_close_are_enforced() { .client() .prepare_mutation( f.binding(2).await, - "publishHistoryCheckpoint".into(), + "publishCheckpoint".into(), original.clone(), scope(&f.issuer, false), Uuid::new_v4(), @@ -301,14 +301,14 @@ async fn host_policy_identity_original_fingerprint_and_close_are_enforced() { changed["expectedRevision"] = json!(Uuid::new_v4()); assert!( f.query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":reference,"input":changed}) ) .await .is_err() ); assert!( - f.mutation(2, "publishHistoryCheckpoint", changed, operation) + f.mutation(2, "publishCheckpoint", changed, operation) .await .is_err() ); @@ -316,7 +316,7 @@ async fn host_policy_identity_original_fingerprint_and_close_are_enforced() { foreign.namespace = id(99); assert!( f.query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":foreign,"input":original}) ) .await @@ -326,7 +326,7 @@ async fn host_policy_identity_original_fingerprint_and_close_are_enforced() { absent.operation = Uuid::new_v4(); let result = f .query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":absent,"input":original}), ) .await @@ -358,7 +358,7 @@ async fn root_reopen_disambiguates_domain_success_without_fabricating_host_clean // A real SQL failure after the separate checkpoint transaction commits. // Install in every possible Host operation partition, not the Session partition. for path in f.runtime_databases() { - rusqlite::Connection::open(path).unwrap().execute_batch("CREATE TRIGGER checkpoint_host_failure BEFORE UPDATE ON application_operations WHEN json_extract(OLD.request_json,'$.method')='publishHistoryCheckpoint' BEGIN SELECT RAISE(ABORT,'fixture lost Host settlement'); END;").unwrap(); + rusqlite::Connection::open(path).unwrap().execute_batch("CREATE TRIGGER checkpoint_host_failure BEFORE UPDATE ON application_operations WHEN json_extract(OLD.request_json,'$.method')='publishCheckpoint' BEGIN SELECT RAISE(ABORT,'fixture lost Host settlement'); END;").unwrap(); } let (reference,receipt)=publish(&f,original.clone(),Uuid::new_v4()).await; assert!(matches!(receipt,Receipt::Unavailable {..}),"{receipt:?}"); @@ -378,7 +378,7 @@ async fn root_reopen_disambiguates_domain_success_without_fabricating_host_clean provide_all(&mut registry).unwrap(); let app=Application::start_from_store(store.clone(),deployment,registry,issuer.clone(),Arc::new(Grant(issuer.clone(),None)),Some(maka_host_js::factory(Default::default()))).await.unwrap(); app.ready().await.unwrap(); - let receipt=query(&app,&issuer,"historyCheckpointReceipt",json!({"reference":reference,"input":original})).await.unwrap(); + let receipt=query(&app,&issuer,"checkpointReceipt",json!({"reference":reference,"input":original})).await.unwrap(); assert_eq!(receipt["publication"]["state"],json!({"kind":"published","head":head})); assert_eq!(receipt["operation"]["outcome"]["kind"],"unresolved"); assert_eq!(receipt["operation"]["cleanup"]["kind"],"unconfirmed"); @@ -426,7 +426,7 @@ async fn cancellation_drains_bounded_reader_and_corruption_never_emits_success() .await .unwrap(); let first = stream.recv().await.unwrap(); - assert_eq!(first["kind"], "native_history_v1"); + assert_eq!(first["kind"], "checkpoint"); stream.cancel(); let completion = stream.completion().await; assert!(completion.result.is_err()); @@ -513,7 +513,7 @@ async fn model_scope_and_missing_recovery_grant_cannot_read_checkpoint_receipts( workspaces::publish_with_policy(&mut f, roots, policy).await; assert_eq!( f.query( - "historyCheckpointReceipt", + "checkpointReceipt", json!({"reference":reference,"input":original}) ) .await diff --git a/plugins/agent/native/src/owner/tests/checkpoints/benchmark.rs b/plugins/agent/native/src/owner/tests/checkpoints/benchmark.rs index b9364e300e5..70d445b0eb6 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints/benchmark.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints/benchmark.rs @@ -193,7 +193,7 @@ async fn history_checkpoint_baseline() { while let Some(frame) = stream.recv().await { first_ms.get_or_insert(time.elapsed().as_secs_f64() * 1000.); match frame["kind"].as_str().unwrap() { - "native_history_v1" => {} + "checkpoint" => {} "chunk" => { assert_eq!(frame["offset"], total); let chunk = STANDARD.decode(frame["data"].as_str().unwrap()).unwrap(); diff --git a/plugins/agent/native/src/owner/tests/checkpoints/load.rs b/plugins/agent/native/src/owner/tests/checkpoints/load.rs index ee88098f4ad..9c0c73f3d6d 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints/load.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints/load.rs @@ -74,10 +74,18 @@ async fn exact_load_reads_frozen_history_and_artifacts_without_changing_live_ses assert_eq!(artifacts["artifacts"].as_array().unwrap().len(),1); let artifact=&artifacts["artifacts"][0];assert_eq!(artifact["name"],"before.txt"); let mut actual=Vec::new(); - while actual.len() Value { + input["workspaceConsistency"] = json!(consistency); + input +} + async fn mutate(f: &Fixture, method: &str, input: Value, id: Uuid) -> (RecoveryRef, Value) { let client = f.app.client(); let prepared = client @@ -58,16 +63,22 @@ async fn workspace_checkpoint_restores_same_session_and_runs_next_turn_then_cas( let session = f.create("Restore workspace").await; std::fs::write(f.workspace.join("answer.txt"), "checkpoint file").unwrap(); turn(&f, &listener, &session, "checkpoint history").await; - let original = json!({"history":input(&f,&session,Value::Null).await,"workspaceConsistency":"unverified"}); - let (publication_ref,value) = mutate(&f,"publishSessionCheckpoint",original.clone(),Uuid::new_v4()).await; - assert_eq!(value["warning"],"external_writers_not_fenced"); - assert_eq!(value["workspaceConsistency"],"unverified"); - let head = value["publication"]["state"]["head"].clone(); + let original = workspace(input(&f,&session,Value::Null).await, "unverified"); + let (publication_ref,value) = mutate(&f,"publishCheckpoint",original.clone(),Uuid::new_v4()).await; + assert_eq!(value["workspace"]["warning"],"external_writers_not_fenced"); + assert_eq!(value["workspace"]["consistency"],"unverified"); + let head = value["state"]["head"].clone(); std::fs::write(f.workspace.join("answer.txt"), "source modified later").unwrap(); - let roots = f.query("directories", json!({"kind":"roots"})).await.unwrap(); + let restore_input = json!({"checkpoint":{"sessionId":session,"revision":head["revision"]}, - "parent":roots["roots"][1]["selection"],"permissions":{"access":"workspace_write","requestPermissions":true}, + "parent":f.workspace,"permissions":{"access":"workspace_write","requestPermissions":true}, "backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + let observed = f.query("observe", json!({"sessionId":session})).await.unwrap(); + let marked = f.mutation(2,"markRead",json!({"sessionId":session,"messageId":observed["session"]["tailMessageId"]}),Uuid::new_v4()).await.unwrap(); + assert_eq!(marked["kind"],"observed"); + let current = f.query("observe", json!({"sessionId":session})).await.unwrap(); + let changed = f.mutation(2,"metadata",json!({"sessionId":session,"expectedRevision":current["session"]["revision"],"title":"Renamed after checkpoint","labels":[],"pinned":true}),Uuid::new_v4()).await.unwrap(); + assert_eq!(changed["kind"],"committed"); let operation = Uuid::new_v4(); let (restore_ref,result) = mutate(&f,"restoreSessionCheckpoint",restore_input.clone(),operation).await; assert_eq!(result["sessionId"],session); @@ -79,15 +90,18 @@ async fn workspace_checkpoint_restores_same_session_and_runs_next_turn_then_cas( let active = f.owner.session(&session).await.unwrap(); assert_eq!(active.configuration.workspace.path,result["path"].as_str().unwrap()); assert_eq!(f.mutation(2,"restoreSessionCheckpoint",restore_input.clone(),operation).await.unwrap(),result); + let repeated = f.mutation(2,"restoreSessionCheckpoint",restore_input.clone(),Uuid::new_v4()).await.unwrap(); + assert_ne!(repeated["path"], result["path"]); + assert_eq!(repeated["head"], head); turn(&f,&listener,&session,"continued after workspace restore").await; let history=f.query("history",json!({"sessionId":session,"position":{"kind":"tail"}})).await.unwrap().to_string(); assert!(history.contains("checkpoint history") && history.contains("continued after workspace restore")); - let next = f.mutation(2,"publishSessionCheckpoint",json!({"history":input(&f,&session,head["revision"].clone()).await,"workspaceConsistency":"caller_confirmed_quiescent"}),Uuid::new_v4()).await.unwrap(); - assert_eq!(next["publication"]["state"]["kind"],"published", "{next}"); - assert_ne!(next["publication"]["state"]["head"]["revision"], head["revision"]); - assert_eq!(f.query("historyCheckpointHead",json!({"sessionId":session})).await.unwrap(),next["publication"]["state"]["head"]); + let next = f.mutation(2,"publishCheckpoint",workspace(input(&f,&session,head["revision"].clone()).await, "caller_confirmed_quiescent"),Uuid::new_v4()).await.unwrap(); + assert_eq!(next["state"]["kind"],"published", "{next}"); + assert_ne!(next["state"]["head"]["revision"], head["revision"]); + assert_eq!(f.query("historyCheckpointHead",json!({"sessionId":session})).await.unwrap(),next["state"]["head"]); assert!(f.mutation(2,"restoreSessionCheckpoint",restore_input.clone(),Uuid::new_v4()).await.is_err()); - assert_eq!(f.query("sessionCheckpointReceipt",json!({"reference":publication_ref,"input":original})).await.unwrap()["checkpoint"],value); + assert_eq!(f.query("checkpointReceipt",json!({"reference":publication_ref,"input":original})).await.unwrap()["publication"],value); let parent=target.parent().unwrap(); std::fs::rename(parent,parent.with_file_name("moved-restored-parent")).unwrap(); assert_eq!(f.query("sessionCheckpointRestoreReceipt",json!({"reference":restore_ref,"input":restore_input})).await.unwrap()["restore"],result); @@ -108,7 +122,7 @@ async fn workspace_checkpoint_requires_explicit_consistency_and_rejects_advanced assert!( f.mutation( 2, - "publishSessionCheckpoint", + "publishCheckpoint", json!({"history":history}), Uuid::new_v4() ) @@ -118,18 +132,14 @@ async fn workspace_checkpoint_requires_explicit_consistency_and_rejects_advanced let value = f .mutation( 2, - "publishSessionCheckpoint", - json!({"history":history,"workspaceConsistency":"caller_confirmed_quiescent"}), + "publishCheckpoint", + workspace(history, "caller_confirmed_quiescent"), Uuid::new_v4(), ) .await .unwrap(); turn(&f, &listener, &session, "newer history").await; - let roots = f - .query("directories", json!({"kind":"roots"})) - .await - .unwrap(); - let request = json!({"checkpoint":{"sessionId":session,"revision":value["publication"]["state"]["head"]["revision"]},"parent":roots["roots"][1]["selection"],"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + let request = json!({"checkpoint":{"sessionId":session,"revision":value["state"]["head"]["revision"]},"parent":f.workspace,"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); assert!( f.mutation(2, "restoreSessionCheckpoint", request, Uuid::new_v4()) .await @@ -160,23 +170,16 @@ async fn workspace_restore_rejects_an_inflight_goal_producer() { let value = f .mutation( 2, - "publishSessionCheckpoint", - json!({ - "history":input(&f,&session,Value::Null).await, - "workspaceConsistency":"unverified" - }), + "publishCheckpoint", + workspace(input(&f, &session, Value::Null).await, "unverified"), Uuid::new_v4(), ) .await .unwrap(); - let roots = f - .query("directories", json!({"kind":"roots"})) - .await - .unwrap(); - let request = json!({"checkpoint":{"sessionId":session,"revision":value["publication"]["state"]["head"]["revision"]},"parent":roots["roots"][1]["selection"],"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); + let request = json!({"checkpoint":{"sessionId":session,"revision":value["state"]["head"]["revision"]},"parent":f.workspace,"permissions":{"access":"read_only","requestPermissions":true},"backend":model_backend(&f.selection().await),"acceptExternalWritersRisk":true}); // A Goal producer can be admitted before restore takes the native runs gate, // yet not have written its first durable record. Its own gate must exclude restore. - let producer = f.owner.goals.gate.lock().await; + let producer = f.owner.goals.admissions.ordered(&session).await; let result = tokio::time::timeout( Duration::from_secs(10), f.mutation( @@ -208,3 +211,60 @@ async fn workspace_restore_rejects_an_inflight_goal_producer() { .unwrap(); close(&f).await; } + +#[tokio::test] +async fn checkpoint_admission_excludes_shared_workspaces_without_stalling_other_sessions() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = Fixture::new( + &format!("http://{}/v1", listener.local_addr().unwrap()), + None, + ) + .await; + let a = f.create("Checkpoint A").await; + let shared = f.create("Shared workspace").await; + let roots = f + .query("directories", json!({"kind":"roots"})) + .await + .unwrap(); + let b = f.mutation(2,"create",json!({"title":"Unrelated B","labels":[],"workspace":{"kind":"directory","selection":roots["roots"][1]["selection"]}}),Uuid::new_v4()).await.unwrap()["session"]["id"].as_str().unwrap().to_owned(); + let shared_producer = f.owner.runs.ordered(&shared).await; + assert!(f.owner.checkpoint_admission(&a).await.is_err()); + drop(shared_producer); + let producer_b = f.owner.goals.admissions.ordered(&b).await; + let (admission, _) = f.owner.checkpoint_admission(&a).await.unwrap(); + assert!(f.owner.session(&a).await.is_err()); + assert!(f.owner.session(&shared).await.is_err()); + assert!(f.owner.checkpoint_admission(&shared).await.is_err()); + drop(producer_b); + let (other_checkpoint, _) = f.owner.checkpoint_admission(&b).await.unwrap(); + drop(other_checkpoint); + tokio::time::timeout( + Duration::from_secs(30), + turn(&f, &listener, &b, "B progresses during A checkpoint"), + ) + .await + .unwrap(); + let current = f.query("observe", json!({"sessionId":b})).await.unwrap(); + let changed = f.mutation(2,"metadata",json!({"sessionId":b,"expectedRevision":current["session"]["revision"],"title":"B still writable","labels":[]}),Uuid::new_v4()).await.unwrap(); + assert_eq!(changed["kind"], "committed"); + drop(admission); + assert!(f.owner.session(&shared).await.is_ok()); + // An admitted run in a distinct directory must not block A's capture. + f.send(&b).await.unwrap(); + let (stream, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + let result = f + .mutation( + 2, + "publishCheckpoint", + workspace(input(&f, &a, Value::Null).await, "unverified"), + Uuid::new_v4(), + ) + .await + .unwrap(); + assert_eq!(result["state"]["kind"], "published"); + drop(stream); + close(&f).await; +} diff --git a/plugins/agent/native/src/owner/title.rs b/plugins/agent/native/src/owner/title.rs index 0f81cd720b8..def4da3ba03 100644 --- a/plugins/agent/native/src/owner/title.rs +++ b/plugins/agent/native/src/owner/title.rs @@ -386,6 +386,11 @@ impl Owner { // Main execution also moves the public revision. Retry metadata only; // each attempt gives queued manual changes and shutdown a turn at the gate. loop { + let _admission = tokio::select! { + biased; + () = cancellation.cancelled() => return Ok(()), + admission = self.runs.ordered(&invocation.session_id) => admission, + }; let runs = tokio::select! { biased; () = cancellation.cancelled() => return Ok(()), diff --git a/plugins/agent/native/src/service.rs b/plugins/agent/native/src/service.rs index b2406e071fe..a964dd58858 100644 --- a/plugins/agent/native/src/service.rs +++ b/plugins/agent/native/src/service.rs @@ -369,19 +369,6 @@ impl Handler for Agent { .preview_history_checkpoint(decode(input)?, context.scope()) .await?, ), - "publishHistoryCheckpoint" => { - let operation = context.operation().ok_or("agent_mutation_required")?; - encode( - owner - .publish_history_checkpoint(decode(input)?, operation, context.scope()) - .await?, - ) - } - "historyCheckpointReceipt" => encode( - owner - .history_checkpoint_receipt(decode(input)?, context) - .await?, - ), "historyCheckpointHead" => encode( owner .history_checkpoint_head(decode(input)?, context.scope()) @@ -411,19 +398,17 @@ impl Handler for Agent { .session_checkpoint_restore_receipt(decode(input)?, context) .await?, ), - "publishSessionCheckpoint" => { + "publishCheckpoint" => { let operation = context.operation().ok_or("agent_mutation_required")?; encode( owner - .publish_session_checkpoint(decode(input)?, operation, context.scope()) + .publish_checkpoint(decode(input)?, operation, context.scope()) .await?, ) } - "sessionCheckpointReceipt" => encode( - owner - .session_checkpoint_receipt(decode(input)?, context) - .await?, - ), + "checkpointReceipt" => { + encode(owner.checkpoint_receipt(decode(input)?, context).await?) + } "historyCheckpointLoadReceipt" => encode( owner .history_checkpoint_load_receipt(decode(input)?, context) @@ -439,11 +424,11 @@ impl Handler for Agent { .loaded_checkpoint_artifacts(decode(input)?, context.scope()) .await?, ), - "loadedHistoryCheckpointArtifact" => checkpoint_output( + "loadedHistoryCheckpointArtifact" => { owner - .loaded_checkpoint_artifact(decode(input)?, context.scope()) - .await?, - ), + .loaded_checkpoint_artifact(decode(input)?, context) + .await + } "fork" => { let operation = context.operation().ok_or("agent_mutation_required")?; encode( diff --git a/products/maka/cli/src/acp/tests/public_client.rs b/products/maka/cli/src/acp/tests/public_client.rs index c144fa1cfdd..8fc72431f7e 100644 --- a/products/maka/cli/src/acp/tests/public_client.rs +++ b/products/maka/cli/src/acp/tests/public_client.rs @@ -130,8 +130,8 @@ async fn published_product_endpoint_exposes_history_checkpoint_and_original_rece tokio::time::timeout(Duration::from_secs(60),async { let mut fixture=Fixture::open().await; let preview=support::query(&fixture.native,"agent","previewHistoryCheckpoint",json!({"sessionId":fixture.session})).await; - let input=json!({"sessionId":fixture.session,"expectedRevision":null,"confirmedSubtreeDigest":preview["subtreeDigest"]}); - let mut call=fixture.native.call("agent","publishHistoryCheckpoint".into(),input.clone(),Default::default()).await.unwrap(); + let input=json!({"sessionId":fixture.session,"expectedRevision":null,"confirmedSubtreeDigest":preview["subtreeDigest"],"workspaceConsistency":null}); + let mut call=fixture.native.call("agent","publishCheckpoint".into(),input.clone(),Default::default()).await.unwrap(); call.start().await.unwrap(); let Reply::Prepared {reference}=call.receive().await.unwrap() else {panic!("original operation reference")}; let Reply::Mutation {reply,cleanup}=call.confirm().await.unwrap() else {panic!("publication receipt")}; @@ -139,7 +139,7 @@ async fn published_product_endpoint_exposes_history_checkpoint_and_original_rece let Receipt::Recorded {outcome:OperationOutcome::Returned {value},..}=reply.receipt else {panic!("publication failed")}; assert_eq!(value["state"]["kind"],"published"); call.close().await.unwrap(); - let receipt=support::query(&fixture.native,"agent","historyCheckpointReceipt",json!({"reference":reference,"input":input})).await; + let receipt=support::query(&fixture.native,"agent","checkpointReceipt",json!({"reference":reference,"input":input})).await; assert_eq!(receipt["publication"],value); let head=support::query(&fixture.native,"agent","historyCheckpointHead",json!({"sessionId":fixture.session})).await; assert_eq!(head,value["state"]["head"]); @@ -150,7 +150,7 @@ async fn published_product_endpoint_exposes_history_checkpoint_and_original_rece loop { match stream.pull().await.unwrap() { Reply::Item {value}=>match value["kind"].as_str().unwrap() { - "native_history_v1"=>assert_eq!(value["head"],head), + "checkpoint"=>assert_eq!(value["head"],head), "chunk"=>{assert_eq!(value["offset"],bytes.len() as u64); bytes.extend(STANDARD.decode(value["data"].as_str().unwrap()).unwrap());}, "end"=>{assert_eq!(value["bytes"],bytes.len() as u64); ended=true;}, _=>panic!("{value}"), From c942cf6a2f9e58834437267979988244817d3c80 Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Fri, 6 Nov 2026 11:14:17 +0800 Subject: [PATCH 5/7] fix(agent): synchronize packaged checkpoint history contracts Regenerate the SDK from the integrated Rust ABI, carry presentation order through frozen history, and resolve restore directories through the path-based workspace API. Generated-by: Codex --- packages/plugin-sdk/src/contracts.ts | 21 ++++++++- packages/plugin-sdk/src/generated/methods.ts | 2 +- .../native/src/owner/checkpoints/restore.rs | 46 ++++++------------- .../src/owner/tests/checkpoints/load.rs | 5 ++ .../src/owner/tests/checkpoints/workspace.rs | 2 +- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/plugin-sdk/src/contracts.ts b/packages/plugin-sdk/src/contracts.ts index db6a0854f28..ace3e081081 100644 --- a/packages/plugin-sdk/src/contracts.ts +++ b/packages/plugin-sdk/src/contracts.ts @@ -13225,7 +13225,7 @@ export const contracts = { }, HistoryPosition: { description: - 'Row reveals an exact field; Before/After continue at byte boundaries and\nEvent addresses the raw event log. Returned chunks retain ascending source\norder, including a reverse window.', + 'Row reveals an exact field; Before/After continue at byte boundaries and\nEvent addresses the raw event log. Returned chunks retain ascending presentation\norder, including a reverse window.', oneOf: [ { type: 'object', @@ -13503,6 +13503,11 @@ export const contracts = { role: { $ref: '#/$defs/Role', }, + order: { + description: + 'Stable display order; position remains the canonical source read address.', + $ref: '#/$defs/HistoryRowPosition', + }, position: { $ref: '#/$defs/HistoryRowPosition', }, @@ -13537,6 +13542,7 @@ export const contracts = { 'turnId', 'timestamp', 'role', + 'order', 'position', 'offset', 'totalBytes', @@ -13872,6 +13878,9 @@ export const contracts = { type: 'string', }, }, + origin: { + $ref: '#/$defs/HistoryToolCallOrigin', + }, presentation: { description: 'Opaque display contract captured at dispatch; no execution authority.', @@ -13908,6 +13917,7 @@ export const contracts = { additionalProperties: false, required: [ 'ancestorToolUseIds', + 'origin', 'position', 'name', 'title', @@ -13915,6 +13925,12 @@ export const contracts = { 'dispatched', ], }, + HistoryToolCallOrigin: { + description: + "Who made a call: a model step, a script the model wrote, a tool's own work\nthrough the Host SDK, or a foreign record.", + type: 'string', + enum: ['provider', 'code_mode', 'host_sdk', 'imported'], + }, DisplayText: { description: 'Presentation text is distinct from the stable identity used for navigation.', @@ -13951,7 +13967,8 @@ export const contracts = { $ref: '#/$defs/HistoryRowPosition', }, isError: { - description: 'Does not distinguish failed from unknown effects.', + description: + 'A failed or unknown effect, or a returned result that reports failure.', type: 'boolean', }, image: { diff --git a/packages/plugin-sdk/src/generated/methods.ts b/packages/plugin-sdk/src/generated/methods.ts index 2c8021afbe8..e278c240dec 100644 --- a/packages/plugin-sdk/src/generated/methods.ts +++ b/packages/plugin-sdk/src/generated/methods.ts @@ -175,7 +175,7 @@ export type AgentLoadHistoryCheckpointOutput_AgentCheckpointWorkspaceWarning = 'external_writers_not_fenced'; /** * Row reveals an exact field; Before/After continue at byte boundaries and - * Event addresses the raw event log. Returned chunks retain ascending source + * Event addresses the raw event log. Returned chunks retain ascending presentation * order, including a reverse window. */ export type AgentLoadedHistoryCheckpointHistoryInput_HistoryPosition = diff --git a/plugins/agent/native/src/owner/checkpoints/restore.rs b/plugins/agent/native/src/owner/checkpoints/restore.rs index bef6ac9451e..9b4f569fe70 100644 --- a/plugins/agent/native/src/owner/checkpoints/restore.rs +++ b/plugins/agent/native/src/owner/checkpoints/restore.rs @@ -108,55 +108,39 @@ impl Owner { return Err("agent_checkpoint_backend_unavailable".into()); } let root = owner.store.owned_root().map_err(failure)?; - let path = std::path::PathBuf::from(&input.parent); - if !path.is_absolute() { - return Err("agent_directory_unavailable".into()); - } - let path = tokio::task::spawn_blocking(move || path.canonicalize()) - .await - .map_err(|_| "agent_directory_unavailable")? - .map_err(|_| "agent_directory_unavailable")?; - if path.starts_with(root.control_directory()) - || path.starts_with(&owner.managed_worktrees) - { - return Err("agent_directory_unavailable".into()); - } - let selection = workspace::selection_at( - owner.workspace_roots.clone(), - path.to_string_lossy().into_owned(), - root.canonical_path().to_owned(), - ) - .await? - .ok_or("agent_directory_unavailable")?; let parent = workspace::observed( owner.workspace_roots.clone(), - selection, + &input.parent, + None, root.canonical_path().to_owned(), ) .await? .ok_or("agent_directory_unavailable")?; + let path = std::path::PathBuf::from(parent.path); + if path.starts_with(root.control_directory()) + || path.starts_with(&owner.managed_worktrees) + { + return Err("agent_directory_unavailable".into()); + } let request = storage::RestoreOperation { operation: operation.operation, fingerprint, revision: storage::Revision(input.checkpoint.revision), - parent: parent.path.into(), - parent_identity: parent.selection.directory_identity, + parent: path, + parent_identity: parent.identity, }; let prepared = repository .prepare_workspace_restore(request.clone()) .await .map_err(failure)?; let root = owner.store.owned_root().map_err(failure)?; - let selection = workspace::selection_at( - owner.workspace_roots.clone(), - prepared.path.to_string_lossy().into_owned(), - root.canonical_path().to_owned(), - ) - .await? - .ok_or("agent_directory_unavailable")?; let selected = workspace::initialize( &owner.workspace_roots, - selection, + prepared + .path + .to_str() + .ok_or("agent_directory_unavailable")?, + None, &owner.managed_worktrees, root.canonical_path(), root.control_directory(), diff --git a/plugins/agent/native/src/owner/tests/checkpoints/load.rs b/plugins/agent/native/src/owner/tests/checkpoints/load.rs index 9c0c73f3d6d..d33ff5e4207 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints/load.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints/load.rs @@ -57,6 +57,10 @@ async fn exact_load_reads_frozen_history_and_artifacts_without_changing_live_ses let bytes=vec![73;65539]; attachments::upload(&f,&session,"before.txt",&bytes,Uuid::new_v4()).await.unwrap(); turn(&f,&listener,&session,"frozen response").await; + let before = f.query("history", json!({"sessionId":session,"position":{"kind":"start"}})).await.unwrap(); + let chunks = before["chunks"].as_array().unwrap(); + assert!(!chunks.is_empty()); + assert!(chunks.iter().all(|chunk| chunk["order"]["sourceSequence"].is_u64())); let checkpoint=published(publish(&f,input(&f,&session,Value::Null).await,Uuid::new_v4()).await.1); let head=checkpoint["state"]["head"].clone(); turn(&f,&listener,&session,"later live response").await; @@ -67,6 +71,7 @@ async fn exact_load_reads_frozen_history_and_artifacts_without_changing_live_ses assert_eq!(result["head"],head); let inspection=result["inspection"].clone(); let page=f.query("loadedHistoryCheckpointHistory",json!({"inspection":inspection,"sessionId":session,"position":{"kind":"start"}})).await.unwrap(); + assert_eq!(page["chunks"], before["chunks"]); let text=page.to_string();assert!(text.contains("frozen response"));assert!(!text.contains("later live response")); let live=f.query("history",json!({"sessionId":session,"position":{"kind":"tail"}})).await.unwrap(); assert!(live.to_string().contains("later live response")); diff --git a/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs b/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs index b48ed83f0ba..3c445015cdd 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs @@ -226,7 +226,7 @@ async fn checkpoint_admission_excludes_shared_workspaces_without_stalling_other_ .query("directories", json!({"kind":"roots"})) .await .unwrap(); - let b = f.mutation(2,"create",json!({"title":"Unrelated B","labels":[],"workspace":{"kind":"directory","selection":roots["roots"][1]["selection"]}}),Uuid::new_v4()).await.unwrap()["session"]["id"].as_str().unwrap().to_owned(); + let b = f.mutation(2,"create",json!({"title":"Unrelated B","labels":[],"workspace":{"kind":"directory","path":roots["roots"][1]["path"]}}),Uuid::new_v4()).await.unwrap()["session"]["id"].as_str().unwrap().to_owned(); let shared_producer = f.owner.runs.ordered(&shared).await; assert!(f.owner.checkpoint_admission(&a).await.is_err()); drop(shared_producer); From 78447d92a6ae5187445e068931f4d8e6aa4c490c Mon Sep 17 00:00:00 2001 From: MicroGrey Date: Fri, 6 Nov 2026 11:22:00 +0800 Subject: [PATCH 6/7] fix(storage): allocate a distinct checkpoint restore migration version Keep the upstream executor-messages migration at version 18. Give the unpublished checkpoint-load tables version 19 so SQLx can initialize and reopen the Host database. Generated-by: Codex --- .../{0018_checkpoint_loads.sql => 0019_checkpoint_loads.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename platform/event-log/migrations/{0018_checkpoint_loads.sql => 0019_checkpoint_loads.sql} (100%) diff --git a/platform/event-log/migrations/0018_checkpoint_loads.sql b/platform/event-log/migrations/0019_checkpoint_loads.sql similarity index 100% rename from platform/event-log/migrations/0018_checkpoint_loads.sql rename to platform/event-log/migrations/0019_checkpoint_loads.sql From e2938770855147e4c5c3750e7b55e98fdfc859c8 Mon Sep 17 00:00:00 2001 From: M4n5ter Date: Fri, 6 Nov 2026 15:38:11 +0800 Subject: [PATCH 7/7] fix(agent): harden checkpoint workspace capture, inspection cache, and session admission Keep read-only session queries accessible during shared-workspace checkpoints while gating mutating entrypoints and background dispatchers, remove arbitrary restore path count caps, cache loaded checkpoint inspections, deduplicate staged object fsyncs, and retry restore metadata writes on concurrent session revisions. Generated-by: Antigravity --- platform/event-log/README.md | 6 +- platform/event-log/README.zh-CN.md | 6 +- platform/event-log/src/bundle/stage.rs | 21 +- platform/event-log/src/checkpoints/restore.rs | 1 - .../event-log/src/checkpoints/workspace.rs | 2 +- platform/event-log/src/root_store.rs | 10 +- .../event-log/src/root_store/checkpoints.rs | 4 +- .../src/root_store/checkpoints/load.rs | 213 ++++++++++++--- .../src/root_store/checkpoints/objects.rs | 36 +++ .../src/root_store/checkpoints/restore.rs | 116 ++++++--- .../src/root_store/checkpoints/tests/load.rs | 91 +++++++ .../root_store/checkpoints/tests/workspace.rs | 196 ++++++++++++-- .../src/root_store/checkpoints/workspace.rs | 111 ++++++-- .../event-log/src/root_store/lifecycle.rs | 10 +- plugins/agent/api/README.md | 6 +- plugins/agent/api/README.zh-CN.md | 10 +- plugins/agent/api/src/session.rs | 19 -- plugins/agent/native/README.zh-CN.md | 4 +- plugins/agent/native/src/owner.rs | 21 +- plugins/agent/native/src/owner/attachments.rs | 2 +- plugins/agent/native/src/owner/checkpoints.rs | 149 ++++++++--- .../native/src/owner/checkpoints/restore.rs | 95 ++++--- .../native/src/owner/checkpoints/session.rs | 14 +- plugins/agent/native/src/owner/compaction.rs | 12 +- plugins/agent/native/src/owner/execution.rs | 14 + plugins/agent/native/src/owner/goal.rs | 11 +- plugins/agent/native/src/owner/graph.rs | 2 +- .../agent/native/src/owner/notifications.rs | 8 + plugins/agent/native/src/owner/patches.rs | 10 +- plugins/agent/native/src/owner/queue.rs | 39 ++- plugins/agent/native/src/owner/runs.rs | 29 ++- .../native/src/owner/scheduler/dispatch.rs | 4 +- plugins/agent/native/src/owner/tests.rs | 25 ++ .../src/owner/tests/checkpoints/workspace.rs | 246 +++++++++++++++++- 34 files changed, 1263 insertions(+), 280 deletions(-) diff --git a/platform/event-log/README.md b/platform/event-log/README.md index 49981dc2d26..6e257f1526a 100644 --- a/platform/event-log/README.md +++ b/platform/event-log/README.md @@ -21,8 +21,8 @@ [简体中文](README.zh-CN.md) -History-only checkpoints use `RootStore::history_checkpoints` and the types in -[`checkpoints`](src/checkpoints.rs). A native Bundle and versioned manifest are +Checkpoints use `RootStore::history_checkpoints` and the types in +[`checkpoints`](src/checkpoints.rs). A native Bundle and manifest are verified and durably stored under the private Root before a separate checkpoint Head is created or conditionally advanced. Head and publication receipt commit in the Session's partition transaction; the Host operation journal can be in a @@ -124,7 +124,7 @@ Integration scenarios share `cargo test -p maka-event-log --test integration`; f Startup recovery reads all unresolved execution authority without a separate item or byte quota; completed history stays excluded. Tool-result payloads and frozen request compositions have no application-level storage size cap. The schema migration preserves existing payloads and composition references. -Workspace snapshots use a versioned inventory plus immutable file objects. Their +Workspace snapshots use a path inventory plus immutable file objects. Their `caller_confirmed_quiescent` / `unverified` declaration is independent of byte integrity and never claims external-writer fencing. Same-Root workspace restore requires unchanged live history, materializes a new directory, and commits the diff --git a/platform/event-log/README.zh-CN.md b/platform/event-log/README.zh-CN.md index 7e47459b8b8..110d2fafd34 100644 --- a/platform/event-log/README.zh-CN.md +++ b/platform/event-log/README.zh-CN.md @@ -23,8 +23,8 @@ Transcript 行分别暴露 `position` 和 `order`:前者定位规范原文,后者按原生 assistant 消息真正开始的位置排序,即使消息结束记录晚于工具调用。历史分页、导航与复制历史按显示顺序使用索引,原文读取和 fence 仍保留规范来源。原生消息正文按 invocation/message 专用索引重建,不逐条重扫此前所有工具输出。 -仅历史 checkpoint 使用 `RootStore::history_checkpoints` 和 -[`checkpoints`](src/checkpoints.rs) 中的类型。原生 Bundle 与版本化 Manifest +Checkpoint 使用 `RootStore::history_checkpoints` 和 +[`checkpoints`](src/checkpoints.rs) 中的类型。原生 Bundle 与 Manifest 先验证并持久保存在私有 Root 内,再创建或条件更新独立的 checkpoint Head。 Head 与发布回执在 Session 所在分区的同一事务提交;Host 操作日志可能位于 另一分区,响应结果未知时必须先查询原发布回执。实时 Session revision 和 @@ -118,4 +118,4 @@ Recall 在 [FTS5](https://sqlite.org/fts5.html#the_detail_option) 中记录每 启动恢复读取全部未决执行状态,不另设条数或字节配额,已完成历史仍不参与扫描。工具结果 payload 与冻结请求配置不设应用层存储大小上限,数据库迁移保留已有内容和配置引用。 -Workspace 存档采用版本化清单和不可变文件对象;`caller_confirmed_quiescent` / `unverified` 声明与字节完整性分开,不能宣称已经隔离外部写入。同 Root 目录恢复要求实时历史未推进,在新目录物化,并原子提交绑定与原回执;不推进 checkpoint Head、不自动执行。文件策略、明确风险提示、故障恢复和范围见 [Agent API](../../plugins/agent/api/README.zh-CN.md)。 +Workspace 存档采用路径清单和不可变文件对象;`caller_confirmed_quiescent` / `unverified` 声明与字节完整性分开,不能宣称已经隔离外部写入。同 Root 目录恢复要求实时历史未推进,在新目录物化,并原子提交绑定与原回执;不推进 checkpoint Head、不自动执行。文件策略、明确风险提示、故障恢复和范围见 [Agent API](../../plugins/agent/api/README.zh-CN.md)。 diff --git a/platform/event-log/src/bundle/stage.rs b/platform/event-log/src/bundle/stage.rs index bb3a0759b67..573b6eef817 100644 --- a/platform/event-log/src/bundle/stage.rs +++ b/platform/event-log/src/bundle/stage.rs @@ -139,13 +139,20 @@ impl StagedBundle { use sha2::{Digest, Sha256}; let mut hash = Sha256::new(); let mut rows = sqlx::query_scalar::<_, i64>( - "SELECT DISTINCT CAST(json_extract(record_json,'$.sequence') AS INTEGER) AS sequence - FROM frames WHERE - (json_extract(record_json,'$.kind')='member' AND json_extract(record_json,'$.session')=?1) - OR (json_extract(record_json,'$.kind')='event' AND - json_extract(json_extract(record_json,'$.json'),'$.invocation.session_id')=?1) - ORDER BY sequence" - ).bind(session).fetch(&mut self.database); + "SELECT sequence FROM ( + SELECT CAST(json_extract(record_json,'$.sequence') AS INTEGER) AS sequence + FROM frames + WHERE kind='member' AND json_extract(record_json,'$.session')=?1 + UNION + SELECT source_sequence AS sequence + FROM frames + WHERE kind='event' + AND json_extract(json_extract(record_json,'$.json'),'$.invocation.session_id')=?1 + ) + ORDER BY sequence", + ) + .bind(session) + .fetch(&mut self.database); while let Some(sequence) = rows.try_next().await? { hash.update(crate::sequence_number(sequence)?.to_le_bytes()); } diff --git a/platform/event-log/src/checkpoints/restore.rs b/platform/event-log/src/checkpoints/restore.rs index 221c8c6fd32..5acbaf26a52 100644 --- a/platform/event-log/src/checkpoints/restore.rs +++ b/platform/event-log/src/checkpoints/restore.rs @@ -32,7 +32,6 @@ pub struct RestoreOperation { #[serde(deny_unknown_fields)] pub struct PreparedRestore { pub head: Head, - pub source_revision: u64, pub history_cut: String, pub workspace: WorkspaceSnapshot, pub path: std::path::PathBuf, diff --git a/platform/event-log/src/checkpoints/workspace.rs b/platform/event-log/src/checkpoints/workspace.rs index cb0f922a631..db0f4a81f29 100644 --- a/platform/event-log/src/checkpoints/workspace.rs +++ b/platform/event-log/src/checkpoints/workspace.rs @@ -41,7 +41,7 @@ pub struct WorkspaceCapture { pub struct WorkspaceSnapshot { pub identity: String, pub consistency: WorkspaceConsistency, - /// Versioned path inventory; file bodies are separately verified immutable objects. + /// Path inventory; file bodies are separately verified immutable objects. pub inventory: ObjectRef, } diff --git a/platform/event-log/src/root_store.rs b/platform/event-log/src/root_store.rs index ac886a6950e..366c2c8496b 100644 --- a/platform/event-log/src/root_store.rs +++ b/platform/event-log/src/root_store.rs @@ -84,6 +84,7 @@ pub struct RootStore { partitions: Vec>, closed: Arc, gates: Arc, + inspections: Arc, lifecycle: Option, project_mutations: Arc>, } @@ -188,6 +189,7 @@ impl RootStore { partitions: Vec::with_capacity(PARTITION_COUNT), closed: Arc::new(AtomicBool::new(false)), gates: gates::Gates::new(), + inspections: checkpoints::load::Inspections::new(), lifecycle: None, project_mutations: Arc::new(tokio::sync::Mutex::new(())), }; @@ -218,7 +220,10 @@ impl RootStore { let logs = std::iter::once(store.control.clone()) .chain(store.partitions.iter().cloned()) .collect(); - store.lifecycle = Some(lifecycle::Lifecycle::start(logs, store.gates.clone()).await?); + store.lifecycle = Some( + lifecycle::Lifecycle::start(logs, store.gates.clone(), store.inspections.clone()) + .await?, + ); // Recovery also owns real workers. Install their independent close // observer before recovery can retain a failed SQL job or stage. store.recover_creations().await?; @@ -282,6 +287,7 @@ impl RootStore { self.closed.store(true, Ordering::Release); self.gates.close(); if self.lifecycle.is_none() { + self.inspections.request_shutdown(); self.control.request_shutdown(); for partition in &self.partitions { partition.request_shutdown(); @@ -297,9 +303,11 @@ impl RootStore { // Bootstrap failures have not installed a lifecycle owner yet. // Observe every real close even if an earlier owner fails. Each failed // owner retains its original authority, so it cannot be replaced. + let cached = self.inspections.take_all(); let results = futures_util::future::join_all( std::iter::once(&self.control) .chain(&self.partitions) + .chain(&cached) .map(|log| log.shutdown()), ) .await; diff --git a/platform/event-log/src/root_store/checkpoints.rs b/platform/event-log/src/root_store/checkpoints.rs index bdfb75c952d..76352f8a69e 100644 --- a/platform/event-log/src/root_store/checkpoints.rs +++ b/platform/event-log/src/root_store/checkpoints.rs @@ -27,7 +27,7 @@ use crate::checkpoints::{ }; use std::future::Future; mod journal; -mod load; +pub(in crate::root_store) mod load; mod objects; pub(crate) mod restore; mod workspace; @@ -37,6 +37,7 @@ use objects::Objects; pub struct HistoryCheckpoints { session: SessionStore, scope: Scope, + inspections: Arc, } #[derive(Debug)] @@ -62,6 +63,7 @@ impl RootStore { Ok(HistoryCheckpoints { session: self.session(session).await?, scope, + inspections: self.inspections.clone(), }) } diff --git a/platform/event-log/src/root_store/checkpoints/load.rs b/platform/event-log/src/root_store/checkpoints/load.rs index 99d832e54a4..9735a88a2e6 100644 --- a/platform/event-log/src/root_store/checkpoints/load.rs +++ b/platform/event-log/src/root_store/checkpoints/load.rs @@ -25,6 +25,91 @@ use sqlx::Connection; use std::collections::BTreeMap; use uuid::Uuid; +const INSPECTION_CACHE_CAPACITY: usize = 8; + +struct CachedInspection { + key: String, + log: Arc, + through: u64, + session_ids: Vec, +} + +enum CacheInsert { + Cached { + active: Arc, + retired: Option>, + }, + Uncached(Arc), +} + +#[derive(Default)] +pub(in crate::root_store) struct Inspections { + entries: std::sync::Mutex>>, +} + +impl Inspections { + pub(in crate::root_store) fn new() -> Arc { + Arc::new(Self::default()) + } + + fn get(&self, key: &str) -> Option> { + let mut entries = self.entries.lock().unwrap(); + let position = entries.iter().position(|entry| entry.key == key)?; + let entry = entries.remove(position); + entries.push(entry.clone()); + Some(entry) + } + + fn insert(&self, candidate: Arc) -> CacheInsert { + let mut entries = self.entries.lock().unwrap(); + if let Some(position) = entries.iter().position(|entry| entry.key == candidate.key) { + let existing = entries.remove(position); + entries.push(existing.clone()); + let duplicate = Arc::try_unwrap(candidate) + .unwrap_or_else(|_| unreachable!("fresh candidate has one owner")) + .log; + return CacheInsert::Cached { + active: existing, + retired: Some(duplicate), + }; + } + if entries.len() >= INSPECTION_CACHE_CAPACITY { + let Some(idle) = entries + .iter() + .position(|entry| Arc::strong_count(entry) == 1) + else { + return CacheInsert::Uncached(candidate); + }; + let evicted = Arc::try_unwrap(entries.remove(idle)) + .unwrap_or_else(|_| unreachable!("idle entry checked under lock")) + .log; + entries.push(candidate.clone()); + return CacheInsert::Cached { + active: candidate, + retired: Some(evicted), + }; + } + entries.push(candidate.clone()); + CacheInsert::Cached { + active: candidate, + retired: None, + } + } + + pub(in crate::root_store) fn request_shutdown(&self) { + for entry in self.entries.lock().unwrap().iter() { + entry.log.request_shutdown(); + } + } + + pub(in crate::root_store) fn take_all(&self) -> Vec> { + std::mem::take(&mut *self.entries.lock().unwrap()) + .into_iter() + .map(|entry| entry.log.clone()) + .collect() + } +} + impl HistoryCheckpoints { fn load_request(&self, load: LoadOperation) -> Result { let request = LoadRequest { @@ -49,7 +134,7 @@ impl HistoryCheckpoints { return Err(invalid("checkpoint load identity conflict")); } return match loaded.state { - LoadState::Preparing => resume(&log, &objects, &request, loaded).await, + LoadState::Preparing => resume(&log, &objects, &request, loaded, None).await, _ => Ok(loaded), }; } @@ -65,7 +150,7 @@ impl HistoryCheckpoints { // Recheck Head at the claim's linearization point. let loaded = claim(&log, request.clone(), selected.head).await?; boundary("load_prepared"); - resume(&log, &objects, &request, loaded).await + resume(&log, &objects, &request, loaded, Some(selected.manifest)).await }) .await } @@ -190,11 +275,34 @@ impl HistoryCheckpoints { let key = load_key(&self.scope, self.session.id(), operation)?; let log = self.session.scoped().await?; let gates = self.session.gates.clone(); + let work = self.session.control_dependency()?; + let pin = gates + .read([gates::Key::CheckpointLoad(key.clone())]) + .await?; + if let Some(entry) = self.inspections.get(&key) { + log.owned_root()?.validate_current()?; + if !entry.session_ids.contains(&session) { + return Err(StoreError::SessionNotFound); + } + let scoped = Arc::new(entry.log.scoped(Arc::new((log, work, pin, entry.clone())))); + return action(scoped, entry.through).await; + } + drop((work, pin)); + let inspections = self.inspections.clone(); + let session_store = self.session.clone(); read_worker(log, gates.clone(), move |log, objects| async move { - let _pin = gates + let pin = gates .read([gates::Key::CheckpointLoad(key.clone())]) .await?; - let (request, loaded) = read(&log, key) + if let Some(entry) = inspections.get(&key) { + if !entry.session_ids.contains(&session) { + return Err(StoreError::SessionNotFound); + } + let work = session_store.control_dependency()?; + let scoped = Arc::new(entry.log.scoped(Arc::new((log, work, pin, entry.clone())))); + return action(scoped, entry.through).await; + } + let (request, loaded) = read(&log, key.clone()) .await? .ok_or_else(|| invalid("checkpoint inspection missing"))?; let LoadState::Ready { @@ -213,7 +321,7 @@ impl HistoryCheckpoints { let target = Arc::new( EventLog::open_checkpoint_inspection(&path, objects.owner.clone(), through).await?, ); - let result = async { + let verified = async { let receipt = target .bundle_import_receipt(transfer_digest, &request.binding()?) .await? @@ -223,10 +331,38 @@ impl HistoryCheckpoints { { return Err(invalid("checkpoint inspection inventory differs")); } - action(target.clone(), through).await + Ok(()) } .await; - close(target, result).await + if let Err(error) = verified { + return close(target, Err(error)).await; + } + let candidate = Arc::new(CachedInspection { + key, + log: target, + through, + session_ids: session_ids.clone(), + }); + match inspections.insert(candidate) { + CacheInsert::Cached { active, retired } => { + if let Some(retired) = retired { + close(retired, Ok(())).await?; + } + let work = session_store.control_dependency()?; + let scoped = Arc::new(active.log.scoped(Arc::new(( + log, + work, + pin, + active.clone(), + )))); + action(scoped, active.through).await + } + CacheInsert::Uncached(entry) => { + let scoped = Arc::new(entry.log.scoped(Arc::new((log, pin)))); + let result = action(scoped, entry.through).await; + close(entry.log.clone(), result).await + } + } }) .await } @@ -257,13 +393,20 @@ impl RootStore { let (request, loaded) = read(&log, key) .await? .ok_or_else(|| invalid("checkpoint load disappeared"))?; - if let Err(error) = resume(&log, &objects, &request, loaded.clone()).await { + if let Err(error) = + resume(&log, &objects, &request, loaded.clone(), None).await + { // Missing/corrupt material does not authorize recapture or block normal startup. // SQL/IO uncertainty keeps Preparing for a later original-material retry. - if matches!( - error, - StoreError::InvalidTransition(_) | StoreError::SessionConflict - ) { + let permanently_unavailable = match &error { + StoreError::InvalidTransition(_) + | StoreError::SessionConflict + | StoreError::EventConflict + | StoreError::ArtifactConflict => true, + StoreError::Io(io) => io.kind() == std::io::ErrorKind::NotFound, + _ => false, + }; + if permanently_unavailable { write( &log, &request, @@ -296,7 +439,7 @@ async fn read( row.map(|(request, result)| Ok((serde_json::from_str(&request)?, serde_json::from_str(&result)?))).transpose() })).await } -async fn claim( +pub(super) async fn claim( log: &EventLog, request: LoadRequest, head: Head, @@ -360,18 +503,6 @@ async fn write( .await } -async fn destination( - objects: &Objects, - request: &LoadRequest, - create: bool, -) -> Result, StoreError> { - // Only this private module can open the inspection database. No live routes, - // execution configuration, recovery scheduler or EventLog handle are exported. - let path = objects.inspection(&request.key()?, create)?; - Ok(Arc::new( - EventLog::open_owned(&path, Some(objects.owner.clone()), OpenPurpose::Execution).await?, - )) -} async fn close(target: Arc, result: Result) -> Result { if let Err(error) = target.shutdown().await { // Keep the physical worker, Root custody and per-inspection gate until SQL ends. @@ -433,18 +564,33 @@ async fn resume( objects: &Objects, request: &LoadRequest, loaded: LoadedCheckpoint, + verified_manifest: Option, ) -> Result { - let source = manifest(objects, request, &loaded)?; + let source = match verified_manifest { + Some(source) => source, + None => { + let source = manifest(objects, request, &loaded)?; + if let Some(snapshot) = source.workspace() { + super::workspace::verify(objects, snapshot)?; + } + source + } + }; let workspace = source.workspace().cloned(); - if let Some(snapshot) = &workspace { - super::workspace::verify(objects, snapshot)?; - } let Manifest { inventory, transfer_digest, .. } = &source; - let target = destination(objects, request, true).await?; + let inspection_path = objects.inspection(&request.key()?, true)?; + let target = Arc::new( + EventLog::open_owned( + &inspection_path, + Some(objects.owner.clone()), + OpenPurpose::Execution, + ) + .await?, + ); let result = async { if target.bundle_import_receipt(transfer_digest, &request.binding()?).await?.is_none() { // An unclaimed nonempty database is never adopted or overwritten. @@ -485,6 +631,13 @@ async fn resume( }) }.await; let state = close(target, result).await?; + std::fs::File::open(&inspection_path)?.sync_all()?; + std::fs::File::open( + inspection_path + .parent() + .ok_or_else(|| invalid("inspection directory missing"))?, + )? + .sync_all()?; boundary("load_imported"); let value = write(log, request, LoadedCheckpoint { state, ..loaded }).await?; boundary("load_recorded"); diff --git a/platform/event-log/src/root_store/checkpoints/objects.rs b/platform/event-log/src/root_store/checkpoints/objects.rs index 2ac89b1f922..f250927f03f 100644 --- a/platform/event-log/src/root_store/checkpoints/objects.rs +++ b/platform/event-log/src/root_store/checkpoints/objects.rs @@ -48,6 +48,11 @@ impl Objects { Ok(Self { owner, directory }) } pub(super) fn existing(owner: Arc) -> Result { + if !cfg!(unix) { + return Err(invalid( + "history checkpoint file durability is not supported on this platform", + )); + } let directory = owner.canonical_path().join("history-checkpoints"); let objects = Self { owner, directory }; objects.validate()?; @@ -90,6 +95,37 @@ impl Objects { self.validate()?; Ok(self.directory.join("objects").join(&reference.digest)) } + pub(super) fn has(&self, reference: &ObjectRef) -> Result { + let path = self.path(reference)?; + match open_regular(&path) { + Ok(mut file) => { + if measure(&mut file)? != *reference { + return Err(invalid("checkpoint object digest or size differs")); + } + Ok(true) + } + Err(StoreError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(error), + } + } + pub(super) fn link_staged(&self, path: &Path, reference: &ObjectRef) -> Result<(), StoreError> { + let destination = self.path(reference)?; + match fs::hard_link(path, destination) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + self.read(reference)?; + } + Err(error) => return Err(error.into()), + } + self.owner.validate_current()?; + Ok(()) + } + pub(super) fn sync_objects(&self) -> Result<(), StoreError> { + self.validate()?; + File::open(self.directory.join("objects"))?.sync_all()?; + self.owner.validate_current()?; + Ok(()) + } pub(super) fn read(&self, reference: &ObjectRef) -> Result { let path = self.path(reference)?; let mut file = open_regular(&path)?; diff --git a/platform/event-log/src/root_store/checkpoints/restore.rs b/platform/event-log/src/root_store/checkpoints/restore.rs index 9b27849b4a5..fb38bce444a 100644 --- a/platform/event-log/src/root_store/checkpoints/restore.rs +++ b/platform/event-log/src/root_store/checkpoints/restore.rs @@ -26,6 +26,7 @@ use serde_json::Value; use sqlx::SqliteConnection; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] struct RequestIdentity { scope: Scope, session: String, @@ -94,12 +95,20 @@ impl Commit { { return Err(invalid("checkpoint restore head moved")); } + let archived: bool = sqlx::query_scalar("SELECT archived FROM session_control WHERE id=?") + .bind(&self.request.session) + .fetch_optional(&mut *db) + .await? + .ok_or(StoreError::SessionNotFound)?; + if archived { + return Err(invalid("archived Session cannot be restored")); + } let inventory = crate::bundle::Inventory { root_session_id: self.request.session.clone(), subtree_digest: String::new(), sessions: vec![crate::bundle::Session { id: self.request.session.clone(), - revision: self.prepared.source_revision, + revision: 0, }], }; crate::bundle::require_idle(db, &inventory).await?; @@ -193,7 +202,7 @@ impl HistoryCheckpoints { let directory = cap_std::fs::Dir::open_ambient_dir(&parent, cap_std::ambient_authority())?; if directory_identity(&directory)? != request.operation.parent_identity { return Err(invalid("restore parent directory changed")); } if parent.starts_with(objects.owner.canonical_path()) { return Err(invalid("restore destination overlaps private Root")); } - let prepared = PreparedRestore { head: checkout.head, source_revision: current.revision, history_cut, workspace, path: parent.join(format!("maka-restored-{}", request.key()?)) }; + let prepared = PreparedRestore { head: checkout.head, history_cut, workspace, path: parent.join(format!("maka-restored-{}", request.key()?)) }; let key = request.key()?; let encoded = serde_json::to_string(&request)?; let data = serde_json::to_string(&prepared)?; @@ -208,7 +217,6 @@ impl HistoryCheckpoints { let identity = directory_identity(&destination)?; workspace::materialize_in(&objects, &prepared.workspace, &destination)?; directory.open(".")?.sync_all()?; - verify_destination(&objects, &prepared, &request.operation, &identity)?; let key = request.key()?; let identity = serde_json::to_string(&identity)?; // A claim alone never proves we created the directory. Publish its @@ -223,16 +231,21 @@ impl HistoryCheckpoints { } /// Configuration and the original restore receipt commit atomically. This /// rebinding never rewinds history or starts an invocation. - pub async fn commit_workspace_restore( + pub async fn commit_workspace_restore( &self, operation: RestoreOperation, - configuration: Value, now: u64, - ) -> Result { + mut update: F, + ) -> Result + where + T: Serialize + serde::de::DeserializeOwned + Clone + PartialEq + Send + 'static, + F: FnMut(&mut T) -> Result<(), StoreError> + Send + 'static, + { crate::sessions::validate_time(now)?; let request = self.restore_identity(operation); let log = self.session.exclusive_metadata(true).await?; let gates = self.session.gates.clone(); + let session_store = self.session.clone(); let control = self .session .control @@ -247,40 +260,56 @@ impl HistoryCheckpoints { } let directory_identity = preparation.directory_identity()?.clone(); let prepared = preparation.prepared; - let reference = crate::workspace_bindings::attached(&configuration)? - .ok_or_else(|| invalid("restore workspace binding missing"))?; - let pin = gates.read([gates::Key::Binding(reference.id)]).await?; verify_destination(&objects, &prepared, &request.operation, &directory_identity)?; - let binding = control - .workspace_binding::(reference.id) - .await? - .ok_or_else(|| invalid("restore binding missing"))?; - if binding.workspace.get("path").and_then(Value::as_str) != prepared.path.to_str() { - return Err(invalid("restore binding differs from prepared workspace")); - } - let key = request.key()?; - let result = log - .mutate_session_metadata::( - &request.session, - prepared.source_revision, - true, - Some(now), - Some(control), - Some(Commit { - key, - request: request.clone(), - prepared: prepared.clone(), - directory_identity, - }), - move |value| { - *value = configuration; - Ok(()) - }, - ) - .await?; - drop(pin); - if !matches!(result, crate::sessions::SessionMutation::Committed(_)) { - return Err(invalid("live Session advanced beyond checkpoint")); + loop { + let record = log + .get_session::(&request.session) + .await? + .ok_or(StoreError::SessionNotFound)?; + let mut normalized = serde_json::to_value(&record.configuration)?; + crate::workspace_bindings::normalize( + &mut normalized, + record.workspace_binding.as_ref(), + )?; + let mut candidate: T = serde_json::from_value(normalized)?; + update(&mut candidate)?; + let encoded = serde_json::to_value(&candidate)?; + let reference = crate::workspace_bindings::attached(&encoded)? + .ok_or_else(|| invalid("restore workspace binding missing"))?; + let pin = gates.read([gates::Key::Binding(reference.id)]).await?; + let binding = control + .workspace_binding::(reference.id) + .await? + .ok_or_else(|| invalid("restore binding missing"))?; + if binding.workspace.get("path").and_then(Value::as_str) != prepared.path.to_str() { + return Err(invalid("restore binding differs from prepared workspace")); + } + let control_dependency = session_store.control_dependency()?; + let key = request.key()?; + let result = log + .scoped(Arc::new((pin, control_dependency))) + .mutate_session_metadata::( + &request.session, + record.revision, + true, + Some(now), + Some(control.clone()), + Some(Commit { + key, + request: request.clone(), + prepared: prepared.clone(), + directory_identity: directory_identity.clone(), + }), + move |value| { + *value = candidate; + Ok(()) + }, + ) + .await?; + match result { + crate::sessions::SessionMutation::Committed(_) => break, + crate::sessions::SessionMutation::RevisionConflict { .. } => continue, + } } prior(&log, &request) .await? @@ -324,6 +353,7 @@ fn verify_destination( fn paths( dir: &Dir, prefix: &str, + expected: &std::collections::BTreeSet, result: &mut std::collections::BTreeSet, ) -> Result<(), StoreError> { for entry in dir.entries()? { @@ -340,18 +370,18 @@ fn verify_destination( } else { format!("{prefix}/{name}") }; - if result.len() >= 20_000 || path.split('/').count() > 65 { - return Err(invalid("restored workspace bounds exceeded")); + if !expected.contains(&path) { + return Err(invalid("restored workspace inventory changed")); } result.insert(path.clone()); if entry.file_type()?.is_dir() { - paths(&dir.open_dir_nofollow(&name)?, &path, result)?; + paths(&dir.open_dir_nofollow(&name)?, &path, expected, result)?; } } Ok(()) } let mut actual = std::collections::BTreeSet::new(); - paths(&root, "", &mut actual)?; + paths(&root, "", &expected, &mut actual)?; if actual != expected { return Err(invalid("restored workspace inventory changed")); } diff --git a/platform/event-log/src/root_store/checkpoints/tests/load.rs b/platform/event-log/src/root_store/checkpoints/tests/load.rs index bb6ab1f4a10..477345fc0b7 100644 --- a/platform/event-log/src/root_store/checkpoints/tests/load.rs +++ b/platform/event-log/src/root_store/checkpoints/tests/load.rs @@ -327,3 +327,94 @@ async fn inspection_reads_share_admission_and_do_not_take_a_writer_lease() { drop(lease); store.close().await.unwrap(); } + +#[tokio::test] +async fn stream_loaded_artifact_handles_multichunk_payloads_and_missing_material_settles_unavailable() + { + use maka_agent_facts::artifact::{Artifact, ArtifactKind, ArtifactSource, content_digest}; + let dir = tempfile::tempdir().unwrap(); + let (store, handle, mut publish) = fixture(dir.path()).await; + let payload: Vec = (0..(96 * 1024)).map(|index| (index % 251) as u8).collect(); + let log = handle.session.scoped().await.unwrap(); + log.commit_artifact( + Artifact { + id: "report-bin".into(), + session_id: "source".into(), + turn_id: "turn-1".into(), + created_at: 1, + name: "report.bin".into(), + kind: ArtifactKind::File, + size_bytes: payload.len() as u64, + mime_type: Some("application/octet-stream".into()), + source: ArtifactSource::UserUpload, + summary: Some(content_digest(&payload)), + }, + payload.clone(), + ) + .await + .unwrap(); + drop(log); + publish.confirmed_subtree_digest = store.preview_bundle("source").await.unwrap().subtree_digest; + let head = published(&handle.publish(publish.clone()).await.unwrap()); + let input = request(head.revision); + handle.load_exact(input.clone()).await.unwrap(); + let mut chunks = Vec::new(); + let mut reconstructed = Vec::new(); + handle + .stream_loaded_artifact( + input.operation, + "source".into(), + "report-bin".into(), + 0, + move |item| { + let (_, offset, chunk) = item.expect("artifact must exist"); + assert_eq!(offset, reconstructed.len() as u64); + chunks.push(chunk.bytes.len()); + reconstructed.extend_from_slice(&chunk.bytes); + let done = reconstructed.len() as u64 == chunk.total_bytes; + if done { + assert_eq!(chunks, vec![32 * 1024, 32 * 1024, 32 * 1024]); + assert_eq!(reconstructed, payload); + } + async { Ok(()) } + }, + ) + .await + .unwrap(); + + // Missing immutable material during Preparing recovery settles to LoadState::Unavailable. + let mut missing_op = input.clone(); + missing_op.operation = Uuid::new_v4(); + let missing_req = LoadRequest { + scope: scope(), + session_id: "source".into(), + load: missing_op.clone(), + }; + let log = handle.session.scoped().await.unwrap(); + super::super::load::claim(&log, missing_req, head.clone()) + .await + .unwrap(); + drop(log); + let objects = Objects::open(store.owned_root().unwrap()).unwrap(); + let manifest = objects.manifest(&head.manifest).unwrap(); + std::fs::remove_file( + dir.path() + .join("root/history-checkpoints/objects") + .join(&manifest.bundle().digest), + ) + .unwrap(); + drop(objects); + store.close().await.unwrap(); + let store = RootStore::open(owner(dir.path())).await.unwrap(); + let handle = store.history_checkpoints("source", scope()).await.unwrap(); + assert_eq!( + handle + .load_receipt(missing_op) + .await + .unwrap() + .unwrap() + .state, + LoadState::Unavailable + ); + store.close().await.unwrap(); +} diff --git a/platform/event-log/src/root_store/checkpoints/tests/workspace.rs b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs index bc4c032c65b..cbf9cf749ad 100644 --- a/platform/event-log/src/root_store/checkpoints/tests/workspace.rs +++ b/platform/event-log/src/root_store/checkpoints/tests/workspace.rs @@ -229,14 +229,73 @@ async fn restore_fixture( .attach_workspace("fixture", &json!({"path":source})) .await .unwrap(); - store - .update_session_metadata("source", 1, move |value: &mut serde_json::Value| { - value["workspace"] = serde_json::to_value(binding)?; - value["boundary_revision"] = json!(1); - Ok(()) - }) + assert!(matches!( + store + .update_session_metadata("source", 1, move |value: &mut serde_json::Value| { + value["workspace"] = serde_json::to_value(binding)?; + value["boundary_revision"] = json!(1); + Ok(()) + }) + .await + .unwrap(), + crate::sessions::SessionMutation::Committed(_) + )); + { + use maka_agent_facts::{ + event::{ + EventWrite, Fact, Invocation, InvocationInput, InvocationOutcome, RuntimeEvent, + }, + input::{DeliveredMessage, MessageInput}, + message::{MessageDisposition, Placement, RootSourceMessage}, + }; + let log = handle.session.scoped().await.unwrap(); + let content: MessageInput = "initial message".to_owned().into(); + let message = RootSourceMessage { + message: DeliveredMessage { + origin: None, + message_id: "msg-read-1".into(), + content: content.clone(), + submitted_content_digest: content.content_digest().unwrap(), + }, + unprepared_content: content.clone(), + submitted_placement: Placement::CurrentTurn, + disposition: MessageDisposition::TurnStarted, + submitted_intent: None, + }; + let invocation = Invocation { + session_id: "source".into(), + turn_id: "turn-1".into(), + run_id: "run-1".into(), + invocation_id: "inv-1".into(), + }; + log.append( + &EventWrite::plain(RuntimeEvent::new( + invocation.clone(), + Fact::InvocationOpened { + configuration: None, + input: InvocationInput::Message { + content, + source_messages: vec![message], + request_fingerprint: None, + }, + }, + )) + .unwrap(), + ) .await .unwrap(); + log.append( + &EventWrite::plain(RuntimeEvent::new( + invocation, + Fact::InvocationEnded { + outcome: InvocationOutcome::Completed, + }, + )) + .unwrap(), + ) + .await + .unwrap(); + } publish.confirmed_subtree_digest = store.preview_bundle("source").await.unwrap().subtree_digest; publish.workspace = Some(WorkspaceCapture { root: source, @@ -266,6 +325,20 @@ async fn restore_fixture( let configuration = json!({"title":"restored","workspace":binding,"boundary_revision":2}); (store, handle, publish, operation, prepared, configuration) } +async fn commit_restore( + handle: &HistoryCheckpoints, + operation: crate::checkpoints::RestoreOperation, + configuration: &serde_json::Value, + now: u64, +) -> Result { + let configuration = configuration.clone(); + handle + .commit_workspace_restore(operation, now, move |value: &mut serde_json::Value| { + *value = configuration.clone(); + Ok(()) + }) + .await +} #[tokio::test] async fn restore_receipt_is_atomic_with_binding_and_survives_later_source_changes() { let directory = tempfile::tempdir().unwrap(); @@ -290,8 +363,7 @@ async fn restore_receipt_is_atomic_with_binding_and_survives_later_source_change log.connection.run(|db|Box::pin(async move {sqlx::raw_sql("CREATE TRIGGER reject_restore BEFORE UPDATE ON checkpoint_workspace_restores BEGIN SELECT RAISE(ABORT,'reject receipt'); END;").execute(db).await?;Ok(())})).await.unwrap(); drop(log); assert!( - handle - .commit_workspace_restore(operation.clone(), configuration.clone(), 2) + commit_restore(&handle, operation.clone(), &configuration, 2) .await .is_err() ); @@ -315,14 +387,12 @@ async fn restore_receipt_is_atomic_with_binding_and_survives_later_source_change .await .unwrap(); drop(log); - let committed = handle - .commit_workspace_restore(operation.clone(), configuration.clone(), 3) + let committed = commit_restore(&handle, operation.clone(), &configuration, 3) .await .unwrap(); assert_eq!(committed.prepared, prepared); assert_eq!( - handle - .commit_workspace_restore(operation.clone(), configuration, 3) + commit_restore(&handle, operation.clone(), &configuration, 3) .await .unwrap(), committed @@ -451,8 +521,7 @@ async fn restore_rejects_head_race_and_changed_destination_without_rebinding() { .unwrap() .unwrap(); assert!( - handle - .commit_workspace_restore(operation.clone(), configuration, 2) + commit_restore(&handle, operation.clone(), &configuration, 2) .await .is_err(), "accepted {race} race" @@ -537,8 +606,7 @@ async fn restore_cannot_commit_a_copy_without_a_durable_completion_record() { .unwrap(); let configuration = json!({"workspace":binding,"boundary_revision":2}); assert!( - handle - .commit_workspace_restore(operation, configuration, 2) + commit_restore(&handle, operation, &configuration, 2) .await .is_err() ); @@ -549,7 +617,14 @@ async fn restore_cannot_commit_a_copy_without_a_durable_completion_record() { async fn workspace_ignores_build_outputs_and_secrets_but_keeps_certificate_fixtures() { let dir = tempfile::tempdir().unwrap(); let source = dir.path().join("project"); - for child in [".git/info", "target", ".venv", "nested", "fixtures"] { + for child in [ + ".git/info", + ".github/workflows", + "target", + ".venv", + "nested", + "fixtures", + ] { std::fs::create_dir_all(source.join(child)).unwrap(); } std::fs::write( @@ -566,6 +641,9 @@ async fn workspace_ignores_build_outputs_and_secrets_but_keeps_certificate_fixtu "local-only", "nested/ignored", ".env", + ".env.local", + ".env.example", + ".github/workflows/ci.yml", "fixtures/test.pem", "fixtures/test.key", "keep.log", @@ -596,6 +674,8 @@ async fn workspace_ignores_build_outputs_and_secrets_but_keeps_certificate_fixtu .map(|entry| entry["path"].as_str().unwrap()) .collect::>(); for retained in [ + ".env.example", + ".github/workflows/ci.yml", "fixtures/test.pem", "fixtures/test.key", "keep.log", @@ -610,6 +690,7 @@ async fn workspace_ignores_build_outputs_and_secrets_but_keeps_certificate_fixtu "local-only", "nested/ignored", ".env", + ".env.local", ".git", ] { assert!(!paths.contains(omitted), "unexpected {omitted}: {paths:?}"); @@ -657,8 +738,7 @@ async fn restore_allows_metadata_changes_and_repeated_restore_of_the_same_histor .unwrap(), ) .unwrap(); - handle - .commit_workspace_restore(request, configuration.clone(), 3) + commit_restore(&handle, request, &configuration, 3) .await .unwrap(); let mut again = original; @@ -669,16 +749,76 @@ async fn restore_allows_metadata_changes_and_repeated_restore_of_the_same_histor .await .unwrap(); assert_ne!(prepared.path, second.path); - configuration["workspace"] = serde_json::to_value( - store - .attach_workspace("fixture", &json!({"path":second.path})) - .await - .unwrap(), - ) - .unwrap(); - handle - .commit_workspace_restore(again, configuration, 4) + // Non-history mutations (read marker + metadata update) between prepare and commit + // must be preserved rather than rejected or overwritten. + store + .set_session_read_marker::("source", "msg-read-1") + .await + .unwrap(); + let live = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + store + .update_session_metadata("source", live.revision, |value: &mut serde_json::Value| { + value["title"] = json!("renamed between prepare and commit"); + Ok(()) + }) .await .unwrap(); + let new_binding = store + .attach_workspace("fixture", &json!({"path":second.path})) + .await + .unwrap(); + let restored = handle + .commit_workspace_restore(again, 4, move |value: &mut serde_json::Value| { + value["workspace"] = serde_json::to_value(new_binding)?; + value["boundary_revision"] = json!(3); + Ok(()) + }) + .await + .unwrap(); + let after = store + .get_session::("source") + .await + .unwrap() + .unwrap(); + assert_eq!(after.revision, restored.session_revision); + assert_eq!( + after.read_state.last_read_message_id.as_deref(), + Some("msg-read-1") + ); + assert_eq!( + after.configuration["title"], + "renamed between prepare and commit" + ); + store.close().await.unwrap(); +} + +#[tokio::test] +async fn case_colliding_paths_fail_before_publishing_a_head() { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("project"); + std::fs::create_dir(&source).unwrap(); + std::fs::write(source.join("Readme.md"), "upper").unwrap(); + let probe = source.join("readme.md"); + std::fs::write(&probe, "lower").unwrap(); + if std::fs::read_to_string(source.join("Readme.md")).unwrap() == "lower" { + // Case-insensitive host filesystem cannot create colliding siblings in the first place. + return; + } + let (store, handle, mut request) = fixture(dir.path()).await; + request.workspace = Some(WorkspaceCapture { + root: source, + identity: "workspace-id".into(), + consistency: WorkspaceConsistency::CallerConfirmedQuiescent, + }); + let outcome = handle.publish(request).await; + assert!( + outcome.is_err(), + "expected case-collision failure: {outcome:?}" + ); + assert!(handle.read_head().await.unwrap().is_none()); store.close().await.unwrap(); } diff --git a/platform/event-log/src/root_store/checkpoints/workspace.rs b/platform/event-log/src/root_store/checkpoints/workspace.rs index c32e242eb15..2b2535f30f0 100644 --- a/platform/event-log/src/root_store/checkpoints/workspace.rs +++ b/platform/event-log/src/root_store/checkpoints/workspace.rs @@ -118,6 +118,7 @@ fn scan(dir: &Dir, root: &Path) -> Result, StoreError> }) .build(); let mut output = BTreeMap::new(); + let mut folded = BTreeSet::new(); for entry in walk { let entry = entry.map_err(|error| invalid(format!("checkpoint workspace scan: {error}")))?; @@ -136,6 +137,9 @@ fn scan(dir: &Dir, root: &Path) -> Result, StoreError> .to_str() .ok_or_else(|| invalid("non-UTF8 checkpoint workspace path"))?; relative(path)?; + if !folded.insert(path.to_lowercase()) { + return Err(invalid("checkpoint workspace path collision")); + } let (parent, name) = parent(dir, path)?; let metadata = parent.symlink_metadata(&name)?; if !metadata.is_dir() && !metadata.is_file() { @@ -182,6 +186,7 @@ pub(super) fn capture( } }); let mut entries = Vec::new(); + let mut linked = false; for (path, expected) in &before { objects.owner.validate_current()?; let (parent, name) = parent(&dir, path)?; @@ -204,31 +209,90 @@ pub(super) fn capture( if !file.metadata()?.is_file() || stamp(&file.metadata()?)? != *expected { return Err(invalid("checkpoint workspace file changed")); } - let mut staged = objects.create(request, "workspace-file")?; - let mut copied = 0u64; + use sha2::{Digest, Sha256}; let mut buffer = [0u8; 65536]; - loop { - let n = file.read(&mut buffer)?; - if n == 0 { - break; + let object = if metadata.len() <= buffer.len() as u64 { + let mut copied = 0usize; + loop { + let n = file.read(&mut buffer[copied..])?; + if n == 0 { + break; + } + copied += n; + if copied as u64 > metadata.len() || copied == buffer.len() { + if copied as u64 > metadata.len() { + return Err(invalid("checkpoint workspace size limit or source change")); + } + break; + } } - copied += n as u64; - if copied > metadata.len() { - return Err(invalid("checkpoint workspace size limit or source change")); + let extra = if copied == buffer.len() { + let mut probe = [0u8; 1]; + file.read(&mut probe)? + } else { + 0 + }; + if extra != 0 + || copied as u64 != metadata.len() + || stamp(&file.metadata()?)? != *expected + || stamp(&parent.symlink_metadata(&name)?)? != *expected + { + return Err(invalid("checkpoint workspace changed during capture")); } - staged.write_all(&buffer[..n])?; - } - if copied != metadata.len() - || stamp(&file.metadata()?)? != *expected - || stamp(&parent.symlink_metadata(&name)?)? != *expected - { - return Err(invalid("checkpoint workspace changed during capture")); - } - staged.sync_all()?; - drop(staged); - let staging = objects.staging(request, "workspace-file")?; - let object = objects.publish(&staging)?; - std::fs::remove_file(staging)?; + let object = ObjectRef { + digest: hex::encode(Sha256::digest(&buffer[..copied])), + bytes: copied as u64, + }; + if !objects.has(&object)? { + let mut staged = objects.create(request, "workspace-file")?; + staged.write_all(&buffer[..copied])?; + staged.sync_all()?; + drop(staged); + let staging = objects.staging(request, "workspace-file")?; + objects.link_staged(&staging, &object)?; + std::fs::remove_file(staging)?; + linked = true; + } + object + } else { + let mut staged = objects.create(request, "workspace-file")?; + let mut hash = Sha256::new(); + let mut copied = 0u64; + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + copied += n as u64; + if copied > metadata.len() { + return Err(invalid("checkpoint workspace size limit or source change")); + } + hash.update(&buffer[..n]); + staged.write_all(&buffer[..n])?; + } + if copied != metadata.len() + || stamp(&file.metadata()?)? != *expected + || stamp(&parent.symlink_metadata(&name)?)? != *expected + { + return Err(invalid("checkpoint workspace changed during capture")); + } + let object = ObjectRef { + digest: hex::encode(hash.finalize()), + bytes: copied, + }; + let staging = objects.staging(request, "workspace-file")?; + if objects.has(&object)? { + drop(staged); + std::fs::remove_file(staging)?; + } else { + staged.sync_all()?; + drop(staged); + objects.link_staged(&staging, &object)?; + std::fs::remove_file(staging)?; + linked = true; + } + object + }; #[cfg(unix)] let executable = { use cap_std::fs::MetadataExt; @@ -250,6 +314,9 @@ pub(super) fn capture( { return Err(invalid("checkpoint workspace changed during capture")); } + if linked { + objects.sync_objects()?; + } let encoded = serde_json::to_vec(&Inventory { entries })?; let mut file = objects.create(request, "workspace")?; file.write_all(&encoded)?; diff --git a/platform/event-log/src/root_store/lifecycle.rs b/platform/event-log/src/root_store/lifecycle.rs index d249e485032..3f250671348 100644 --- a/platform/event-log/src/root_store/lifecycle.rs +++ b/platform/event-log/src/root_store/lifecycle.rs @@ -32,6 +32,7 @@ impl Lifecycle { pub(super) async fn start( logs: Vec>, gates: Arc, + inspections: Arc, ) -> Result { let (ready, started) = tokio::sync::oneshot::channel(); let (done, closed) = tokio::sync::oneshot::channel(); @@ -45,6 +46,7 @@ impl Lifecycle { { Ok(runtime) => runtime, Err(error) => { + inspections.request_shutdown(); for log in &logs { log.request_shutdown(); } @@ -59,6 +61,7 @@ impl Lifecycle { if ready.send(Ok(())).is_err() { gates.close(); } + let mut cached_inspections = Vec::new(); let result = std::panic::catch_unwind(AssertUnwindSafe(|| { let (unconfirmed, premature_close) = runtime.block_on(async { let receipts = logs.iter().map(|log| log.close_receipt()).collect::>(); @@ -88,8 +91,9 @@ impl Lifecycle { // can still persist known results before quarantining. return Err("accepted Root work cleanup is unconfirmed".into()); } - for log in &logs { log.request_shutdown(); } - let results = runtime.block_on(futures_util::future::join_all(logs.iter().map(|log| log.shutdown()))); + cached_inspections = inspections.take_all(); + for log in logs.iter().chain(&cached_inspections) { log.request_shutdown(); } + let results = runtime.block_on(futures_util::future::join_all(logs.iter().chain(&cached_inspections).map(|log| log.shutdown()))); let mut failures: Vec = results.into_iter().filter_map(Result::err).map(|error| error.to_string()).collect(); if let Some(error) = premature_close { failures.push(error); } if failures.is_empty() { @@ -103,6 +107,8 @@ impl Lifecycle { }); let failed = result.is_err(); if !failed { + drop(cached_inspections); + drop(inspections); drop(logs); drop(gates); } diff --git a/plugins/agent/api/README.md b/plugins/agent/api/README.md index 0b1ed33714e..614b9e80403 100644 --- a/plugins/agent/api/README.md +++ b/plugins/agent/api/README.md @@ -83,6 +83,10 @@ The [checkpoint contract](src/contract/checkpoints.rs) exposes explicit checkpoi | `checkpointReceipt` | Query | Original Host observation and domain publication receipt | | `historyCheckpointHead` | Query | Current checkpoint head, or null | | `readHistoryCheckpoint` | Subscription | Exact retained native history bundle | +| `loadHistoryCheckpoint` | Mutation | Isolated read-only inspection state and member Session IDs | +| `historyCheckpointLoadReceipt` | Query | Original Host observation and domain load receipt | +| `restoreSessionCheckpoint` | Mutation | Rebound same-Root Session and materialized Workspace path | +| `sessionCheckpointRestoreReceipt` | Query | Original Host observation and domain restore receipt | Preview the source, then publish with its `confirmedSubtreeDigest` and an explicit `expectedRevision` (null only for the first head). The digest confirms membership, not an earlier content revision; the manifest records the actual consistent cut. Busy sources fail instead of queuing. Live messages do not advance the checkpoint head. Publication uses ordinary Host mutation preparation/confirmation; retain the original `RecoveryRef` **and input** before confirmation. Never replace an uncertain operation with a new ID. A CAS conflict is terminal for that operation; a newly chosen expectation requires a new operation. @@ -120,7 +124,7 @@ Capture honors project `.gitignore`, nested ignore files and `.git/info/exclude` The API and private JSON are development contracts: no legacy publication endpoints, manifest variants or implicit missing-Workspace defaults are retained. Existing development checkpoint data in the former shape requires explicit offline migration or recreation. -`restoreSessionCheckpoint` takes `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`. Supply an absolute path under an allowed directory root; physical identity is captured and rechecked by the Host, not supplied by the caller. the Host creates a **new** `maka-restored-…` child and never overwrites the original project. Only an ordinary native Session in the **same Root**, still at the captured event-history cut, is supported. Read markers, titles, pins and prior restores do not invalidate unchanged history; the current metadata revision is used only for the restore commit CAS. Advanced history, moved checkpoint Head, WorkHub, child Sessions, Goal state and executor backends are rejected. The target model and permissions are explicit; old grants are retired. An unverified source remains unverified after restoration. +`restoreSessionCheckpoint` takes `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`. Supply an absolute or `~`-prefixed path under an allowed directory root; physical identity is captured and rechecked by the Host, not supplied by the caller. The Host creates a **new** `maka-restored-…` child and never overwrites the original project. Only an ordinary native Session in the **same Root**, still at the captured event-history cut, is supported. Read markers, titles, pins and prior restores do not invalidate unchanged history; the current metadata revision is used only for the restore commit CAS. Advanced history, moved checkpoint Head, WorkHub, child Sessions, Goal state and executor backends are rejected. The target model and permissions are explicit; old grants are retired. An unverified source remains unverified after restoration. The restore atomically commits the new workspace binding and original restore receipt. It keeps the Session ID and existing checkpoint Head; a later publication uses that Head's revision as its CAS expectation. It does **not** roll back history or reconstruct a lost Root. Loading/rebinding does not execute a model or tool. Send an ordinary new message explicitly to continue; it uses the new Workspace. Cross-Root/Host authority handoff, arbitrary historical rollback, full auxiliary-state migration and mid-turn recovery are outside this implementation. diff --git a/plugins/agent/api/README.zh-CN.md b/plugins/agent/api/README.zh-CN.md index 32fe0c0f983..5123c871ad3 100644 --- a/plugins/agent/api/README.zh-CN.md +++ b/plugins/agent/api/README.zh-CN.md @@ -83,6 +83,10 @@ macOS 上,活动订阅还会比较直接子项的元数据。每次扫描后 | `checkpointReceipt` | Query | 原 Host 操作观测及领域发布回执 | | `historyCheckpointHead` | Query | 当前 checkpoint Head,或 null | | `readHistoryCheckpoint` | Subscription | 指定 retained revision 的原生历史包 | +| `loadHistoryCheckpoint` | Mutation | 隔离只读检查区状态及导入的成员 Session ID | +| `historyCheckpointLoadReceipt` | Query | 原 Host 操作观测及领域加载回执 | +| `restoreSessionCheckpoint` | Mutation | 同 Root 重新绑定后的 Session 及恢复目录路径 | +| `sessionCheckpointRestoreReceipt` | Query | 原 Host 操作观测及领域恢复回执 | 先预览,再携带 `confirmedSubtreeDigest` 和显式 `expectedRevision` 发布;只有创建第一个 Head 时预期为 null。摘要确认成员集合,不固定此前的内容 revision;Manifest 记录实际一致捕获的切点。繁忙源直接失败,不进入等待队列。日常消息不推进 checkpoint Head。发布经过普通 Host mutation 的准备与确认,确认前保存原 `RecoveryRef` **及原始输入**。结果未知时查询原操作,不换 ID 重发。CAS 冲突对原操作是终态,修改期望需要新操作。 @@ -108,7 +112,7 @@ macOS 上,活动订阅还会比较直接子项的元数据。每次扫描后 | `loadedHistoryCheckpointArtifacts` | `offset` | 最多 32 条 Artifact 元数据及 `nextOffset` | | `loadedHistoryCheckpointArtifact` | `artifactId`、字节 `offset` | 订阅:连续返回元数据及最多 32 KiB 的 Base64 块;不存在返回 null | -使用返回的历史游标和 Artifact 偏移分页,无需把整个 checkpoint 放入内存。检查副本独立于后续 Head 变化保留。目前检查数据库和不可变对象没有删除/GC API;不断发起**新**加载会增加磁盘占用,原操作重试复用原目标。这些方法与发布使用相同的管理授权和 namespace;定位信息本身不授予访问权。 +使用返回的历史游标和 Artifact 偏移分页,无需把整个 checkpoint 放入内存。每个 Artifact 订阅在完整传输期间只使用一条只读数据库连接,具备背压与取消排空;调用方须确认订阅完成及清理结果。检查区读取不申请写租约、不执行迁移、不同步目录,可并发读取。检查副本独立于后续 Head 变化保留。目前检查数据库和不可变对象没有删除/GC API;不断发起**新**加载会增加磁盘占用,原操作重试复用原目标。这些方法与发布使用相同的管理授权和 namespace;定位信息本身不授予访问权。 ### Workspace 发布与本地继续执行 @@ -120,9 +124,7 @@ macOS 上,活动订阅还会比较直接子项的元数据。每次扫描后 API 和私有 JSON 仍是开发中契约:不保留旧发布端点、Manifest 双轨或缺失 Workspace 字段的隐式兼容。旧形状的开发 checkpoint 数据需要显式离线迁移或重新生成。 -`loadHistoryCheckpoint` 只验证 Workspace 对象并返回一致性元数据,不复制 Workspace。历史及 Artifact 列表按游标分页;`loadedHistoryCheckpointArtifact` 是订阅,从指定 offset 连续传输最多 32 KiB 的块,完整传输只打开一次只读数据库连接。读取不申请写租约、不执行迁移、不同步目录,可并发读取;调用方仍须确认订阅完成及清理结果。 - -`restoreSessionCheckpoint` 输入为 `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`。输入为获准根内的绝对路径,目录物理身份由 Host 获取并重新验证。Host 在父目录中新建 `maka-restored-…` 子目录,不覆盖原项目。首版仅支持**同 Root、事件历史仍与存档点相同**的普通 native Session;已读、改名、置顶、之前的恢复不会使未变化的历史失效,当前元数据 revision 仅用于提交 CAS。历史已推进、Head 已移动、WorkHub、子 Session、Goal 状态和 executor backend 都拒绝。显式选择目标模型与权限,撤销旧授权;unverified 不会因恢复变成可信。 +`restoreSessionCheckpoint` 输入为 `{ checkpoint: { sessionId, revision }, parent: "/absolute/allowed/directory", permissions, backend, acceptExternalWritersRisk: true }`。输入为获准根内的绝对路径或 `~` 前缀路径,目录物理身份由 Host 获取并重新验证。Host 在父目录中新建 `maka-restored-…` 子目录,不覆盖原项目。首版仅支持**同 Root、事件历史仍与存档点相同**的普通 native Session;已读、改名、置顶、之前的恢复不会使未变化的历史失效,当前元数据 revision 仅用于提交 CAS。历史已推进、Head 已移动、WorkHub、子 Session、Goal 状态和 executor backend 都拒绝。显式选择目标模型与权限,撤销旧授权;unverified 不会因恢复变成可信。 恢复在同一事务提交目录绑定与原操作回执,保留 Session ID 和 checkpoint Head,下一次发布仍以原 Head revision 做 CAS。不会回滚新历史,也不重建已丢失的 Root;加载/重新绑定不会自动调用模型或工具,显式发送下一条普通消息后才从新目录执行。跨 Root/Host 权威交接、任意历史回滚、完整辅助状态迁移和轮次中间恢复不在本次范围。 diff --git a/plugins/agent/api/src/session.rs b/plugins/agent/api/src/session.rs index b3340fb1f4f..9983221a8fc 100644 --- a/plugins/agent/api/src/session.rs +++ b/plugins/agent/api/src/session.rs @@ -107,23 +107,4 @@ impl ResolvedConfiguration { binding: self.binding, } } - - /// Attach to a new binding after the old effective boundary was resolved. - /// The Session metadata transaction validates the reference's current epochs. - pub fn into_stored(self, workspace: BindingRef) -> StoredSessionConfiguration { - StoredSessionConfiguration { - parent_session_id: self.parent_session_id, - backend: self.backend, - collaboration_mode: self.collaboration_mode, - tools: self.tools, - compaction_prompt: self.compaction_prompt, - title: self.title, - title_is_manual: self.title_is_manual, - pinned: self.pinned, - labels: self.labels, - workspace, - permissions: self.permissions, - boundary_revision: self.boundary_revision, - } - } } diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md index de2df0cba57..6ad93e6f1e6 100644 --- a/plugins/agent/native/README.zh-CN.md +++ b/plugins/agent/native/README.zh-CN.md @@ -28,7 +28,7 @@ 参见 [API 工作流](../api/README.zh-CN.md#历史-checkpoint-管理)和[手动性能基线](src/owner/tests/checkpoints/benchmark.rs)。 模型工具和跨 Root 恢复激活不在此 API 范围内;Workspace 与同 Root 恢复见下文。 -[检查适配层](src/owner/checkpoints/load.rs)通过相同 Host 管理边界提供精确加载及有界历史、Artifact 查询。加载的历史保留原 ID,放在隔离存储中,不向本 Owner 的执行或消息恢复注册。响应未知时使用原操作及原输入查询 `historyCheckpointLoadReceipt`。加载本身不授予执行能力;同 Root 恢复要求显式目标权限与模型,跨 Host 身份交接及系统级外部停写仍未提供。 +[检查适配层](src/owner/checkpoints/load.rs)通过相同 Host 管理边界提供精确加载及有界历史、Artifact 查询。加载的历史保留原 ID,放在隔离存储中,不向本 Owner 的执行或消息恢复注册。响应未知时使用原操作及原输入查询 `historyCheckpointLoadReceipt`。Workspace 发布与显式的同 Root 目录恢复分别见 [Session 适配器](src/owner/checkpoints/session.rs)和[恢复适配器](src/owner/checkpoints/restore.rs)。这两者保留条件一致性提示,要求明确的目标权限与模型,不自动发起消息。跨 Root 恢复及系统级外部停写仍未提供,详见 [API 支持范围](../api/README.zh-CN.md#workspace-发布与本地继续执行)。 `agent.fork` 捕获源会话版本和显式 `position`:`end`、带逻辑 Turn ID 的 `through_turn` 或 `before_turn`,以及表示空历史的 `start`。`before_turn` 排除目标完整轮次,指定第一轮可创建空对话。Host 在复制事务中从规范历史计算边界,客户端无需寻找上一轮或依赖采样的历史导航点。返回的分支保留该位置,源会话保持不变。 @@ -162,5 +162,3 @@ ChatGPT 冷缓存或到期刷新在逻辑请求准备时等待,后台压缩的 订阅 Provider 仅在连接开启该选项时,在解析后的能力中选择原生压缩。关闭后仍可回放已有原生 checkpoint;下一次压缩生成文字摘要。Host 冻结一个协议操作,Responses 适配器负责追加触发命令, 并只在流完成时返回有类型的 `NativeCompaction`。Engine 和 EventLog 无需再从 provider options JSON 提取结果。 检查点头部只保留来源步骤引用、内容摘要和 token 估算,baseline 读取时从原始请求派生 producer/model。 - -Workspace 发布与显式的同 Root 目录恢复分别见 [Session 适配器](src/owner/checkpoints/session.rs)和[恢复适配器](src/owner/checkpoints/restore.rs)。这两者保留条件一致性提示,要求明确的目标权限与模型,不自动发起消息。跨 Root 恢复仍未提供,详见 [API 支持范围](../api/README.zh-CN.md)。 diff --git a/plugins/agent/native/src/owner.rs b/plugins/agent/native/src/owner.rs index 298d3b9a540..80e623e21a2 100644 --- a/plugins/agent/native/src/owner.rs +++ b/plugins/agent/native/src/owner.rs @@ -173,7 +173,7 @@ struct Control { } #[derive(Default)] struct Runs { - checkpoints: Arc>>, + checkpoints: Arc>>>, closing: bool, controls: BTreeMap, preparing: BTreeMap, @@ -737,16 +737,22 @@ impl Owner { if !self.owns(id) { return Err("agent_session_unavailable".into()); } - if self.runs.checkpointed(id) { - return Err("agent_checkpoint_busy".into()); - } let session = self .store .get_session::(id) .await .map_err(|_| "agent_store_unavailable".to_owned())? .ok_or("agent_session_unavailable")?; - let session = resolve_session(session)?; + resolve_session(session) + } + pub(in crate::owner) async fn mutable_session( + &self, + id: &str, + ) -> Result, String> { + if self.runs.checkpointed(id) { + return Err("agent_checkpoint_busy".into()); + } + let session = self.session(id).await?; if !self.runs.workspace_available( id, std::path::Path::new(&session.configuration.workspace.path), @@ -1279,7 +1285,7 @@ impl Owner { .as_ref() .map(|parked| parked.checkpoint.invocation.clone()) }); - let session = self.session(&id).await?; + let session = self.mutable_session(&id).await?; if session.archived { return Err("agent_session_configuration_changed".into()); } @@ -1472,7 +1478,7 @@ impl Owner { .map_err(|_| "agent_execution_unavailable")? .map(|parked| parked.checkpoint.invocation), }; - let current = self.session(&id).await?; + let current = self.mutable_session(&id).await?; if current.configuration != session.configuration || current.archived { return Err("agent_session_configuration_changed".into()); } @@ -1705,6 +1711,7 @@ impl Owner { let runs = self.runs.clone().try_lock_owned().ok()?; if !runs.controls.is_empty() || !runs.preparing.is_empty() + || self.runs.has_checkpoints() || !self.workers.is_empty() || self.auxiliary.resources.has_pending() || self.goals.keeps_host_awake() diff --git a/plugins/agent/native/src/owner/attachments.rs b/plugins/agent/native/src/owner/attachments.rs index 11e3a8e2082..edf5be496a2 100644 --- a/plugins/agent/native/src/owner/attachments.rs +++ b/plugins/agent/native/src/owner/attachments.rs @@ -62,7 +62,7 @@ impl Owner { reason: AttachmentRejection::Closed, }); } - if owner.session(&input.session_id).await?.archived { + if owner.mutable_session(&input.session_id).await?.archived { return Ok(AttachmentUploadResult::Rejected { reason: AttachmentRejection::Archived, }); diff --git a/plugins/agent/native/src/owner/checkpoints.rs b/plugins/agent/native/src/owner/checkpoints.rs index a5146d05534..da58053f260 100644 --- a/plugins/agent/native/src/owner/checkpoints.rs +++ b/plugins/agent/native/src/owner/checkpoints.rs @@ -31,15 +31,44 @@ use maka_plugin_core::operation::RecoveryRef; use tokio::io::AsyncReadExt; pub(super) struct CheckpointAdmission { + _workspace: runs::Checkpoint, _session: runs::Ordered, _goal: runs::Ordered, - _workspace: runs::Checkpoint, + _wake: WakeAfterCheckpoint, +} + +struct WakeAfterCheckpoint(std::sync::Weak); + +impl Drop for WakeAfterCheckpoint { + fn drop(&mut self) { + if let Some(owner) = self.0.upgrade() { + owner.wake_after_checkpoint(); + } + } } impl Owner { + fn wake_after_checkpoint(self: &Arc) { + if self.stop.is_cancelled() || self.workers.is_closed() { + return; + } + self.goals.execution_changed(); + if tokio::runtime::Handle::try_current().is_ok() { + let owner = self.clone(); + self.workers.spawn(async move { + if owner.stop.is_cancelled() { + return; + } + owner.schedule().await; + let _ = owner.schedule_activations(now()).await; + }); + } + } + pub(super) async fn checkpoint_admission( self: &Arc, id: &str, + extra_path: Option<&std::path::Path>, ) -> Result<(CheckpointAdmission, SessionRecord), String> { let goal = self .goals @@ -50,22 +79,57 @@ impl Owner { .runs .try_ordered(id) .map_err(|_| "agent_checkpoint_busy")?; - let runs = self.runs.state().await; - if runs.closing || runs.busy(id) || runs.retained_cleanup.contains(id) { + let session = self.session(id).await?; + let mut paths = vec![std::path::PathBuf::from( + &session.configuration.workspace.path, + )]; + if let Some(extra) = extra_path { + let extra = extra.to_path_buf(); + if !paths.contains(&extra) { + paths.push(extra); + } + } + let workspace = self.runs.checkpoint(id, paths.clone())?; + let admission = CheckpointAdmission { + _workspace: workspace, + _session: ordered, + _goal: goal, + _wake: WakeAfterCheckpoint(Arc::downgrade(self)), + }; + let mut active = { + let runs = self.runs.state().await; + if runs.closing + || self.stop.is_cancelled() + || runs.controls.contains_key(id) + || runs.preparing.contains_key(id) + || runs.retained_cleanup.contains(id) + { + return Err("agent_checkpoint_busy".into()); + } + runs.controls + .keys() + .chain(runs.preparing.keys()) + .chain(runs.retained_cleanup.iter()) + .cloned() + .chain(self.runs.ordered_sessions()) + .chain(self.goals.admissions.ordered_sessions()) + .filter(|active| active != id) + .collect::>() + }; + if self.has_parked_invocation(id).await? + || !self + .store + .session(id) + .await + .map_err(|_| "agent_store_unavailable")? + .message_queue(id) + .await + .map_err(|_| "agent_store_unavailable")? + .entries + .is_empty() + { return Err("agent_checkpoint_busy".into()); } - let session = self.session(id).await?; - let path = std::path::PathBuf::from(&session.configuration.workspace.path); - let mut active = runs - .controls - .keys() - .chain(runs.preparing.keys()) - .chain(runs.retained_cleanup.iter()) - .cloned() - .chain(self.runs.ordered_sessions()) - .chain(self.goals.admissions.ordered_sessions()) - .filter(|active| active != id) - .collect::>(); let mut after = None; loop { let pending = self @@ -79,35 +143,41 @@ impl Owner { after = pending.last().cloned(); active.extend(pending.into_iter().filter(|active| active != id)); } + active.extend( + self.store + .ready_native_approval_sessions() + .await + .map_err(|_| "agent_store_unavailable")? + .into_iter() + .filter(|active| self.owns(active) && active != id), + ); + active.extend( + self.store + .due_model_retry_sessions(i64::MAX as u64) + .await + .map_err(|_| "agent_store_unavailable")? + .into_iter() + .filter(|active| self.owns(active) && active != id), + ); for active in active { - // Existing checkpoint reservations are compared atomically below. if self.runs.checkpointed(&active) { continue; } - let other = resolve_session( - self.store - .get_session::(&active) - .await - .map_err(|_| "agent_store_unavailable")? - .ok_or("agent_session_unavailable")?, - )?; - if runs::overlaps( - &path, - std::path::Path::new(&other.configuration.workspace.path), - ) { + let Some(other) = self + .store + .get_session::(&active) + .await + .map_err(|_| "agent_store_unavailable")? + else { + continue; + }; + let other = resolve_session(other)?; + let other_path = std::path::Path::new(&other.configuration.workspace.path); + if paths.iter().any(|path| runs::overlaps(path, other_path)) { return Err("agent_checkpoint_busy".into()); } } - let workspace = self.runs.checkpoint(id, path)?; - drop(runs); - Ok(( - CheckpointAdmission { - _session: ordered, - _goal: goal, - _workspace: workspace, - }, - session, - )) + Ok((admission, session)) } } @@ -255,7 +325,10 @@ impl Owner { } fn management(scope: &Scope) -> Result<(), String> { - if scope.identity.agent().is_some() { + if !matches!( + scope.identity, + maka_capabilities::call::Identity::Remote { .. } + ) { return Err("agent_history_checkpoint_user_action_required".into()); } Ok(()) diff --git a/plugins/agent/native/src/owner/checkpoints/restore.rs b/plugins/agent/native/src/owner/checkpoints/restore.rs index 9b4f569fe70..aa528980048 100644 --- a/plugins/agent/native/src/owner/checkpoints/restore.rs +++ b/plugins/agent/native/src/owner/checkpoints/restore.rs @@ -82,18 +82,51 @@ impl Owner { if !input.accept_external_writers_risk { return Err("agent_checkpoint_external_writers_not_fenced".into()); } + if !matches!(input.backend, BackendPreference::Model { .. }) { + return Err("agent_checkpoint_unsupported_session".into()); + } + if !owner.backend_available(&input.backend).await? { + return Err("agent_checkpoint_backend_unavailable".into()); + } + let root = owner.store.owned_root().map_err(failure)?; + let parent = workspace::observed( + owner.workspace_roots.clone(), + &input.parent, + owner.home.as_deref(), + root.canonical_path().to_owned(), + ) + .await? + .ok_or("agent_directory_unavailable")?; + let path = std::path::PathBuf::from(parent.path); + let control_dir = std::fs::canonicalize(root.control_directory()) + .unwrap_or_else(|_| root.control_directory().to_owned()); + if path.starts_with(&control_dir) + || path.starts_with(root.control_directory()) + || path.starts_with(&owner.managed_worktrees) + { + return Err("agent_directory_unavailable".into()); + } let (admission, session) = owner - .checkpoint_admission(&input.checkpoint.session_id) + .checkpoint_admission(&input.checkpoint.session_id, Some(&path)) .await?; if owner.behavior.is_some() || session.archived || session.configuration.parent_session_id.is_some() - || !matches!(input.backend, BackendPreference::Model { .. }) || session .configuration .backend .as_ref() .is_some_and(|v| !matches!(v, BackendPreference::Model { .. })) + || owner.graph_selected(&session.id).await? + || owner + .store + .session(&session.id) + .await + .map_err(|e| e.to_string())? + .session_manager(&session.id) + .await + .map_err(|_| "agent_session_unavailable")? + .is_some() || owner .goals .repository @@ -104,24 +137,6 @@ impl Owner { { return Err("agent_checkpoint_unsupported_session".into()); } - if !owner.backend_available(&input.backend).await? { - return Err("agent_checkpoint_backend_unavailable".into()); - } - let root = owner.store.owned_root().map_err(failure)?; - let parent = workspace::observed( - owner.workspace_roots.clone(), - &input.parent, - None, - root.canonical_path().to_owned(), - ) - .await? - .ok_or("agent_directory_unavailable")?; - let path = std::path::PathBuf::from(parent.path); - if path.starts_with(root.control_directory()) - || path.starts_with(&owner.managed_worktrees) - { - return Err("agent_directory_unavailable".into()); - } let request = storage::RestoreOperation { operation: operation.operation, fingerprint, @@ -134,16 +149,18 @@ impl Owner { .await .map_err(failure)?; let root = owner.store.owned_root().map_err(failure)?; + let control_dir = std::fs::canonicalize(root.control_directory()) + .unwrap_or_else(|_| root.control_directory().to_owned()); let selected = workspace::initialize( &owner.workspace_roots, prepared .path .to_str() .ok_or("agent_directory_unavailable")?, - None, + owner.home.as_deref(), &owner.managed_worktrees, root.canonical_path(), - root.control_directory(), + &control_dir, ) .await? .ok_or("agent_workspace_unavailable")?; @@ -152,27 +169,27 @@ impl Owner { .attach_workspace(&owner.prefix, &selected) .await .map_err(failure)?; - let mut configuration = owner - .store - .get_session::(&session.id) - .await - .map_err(failure)? - .ok_or("agent_session_unavailable")? - .configuration; - configuration.workspace = binding; - configuration.permissions = input.permissions; - configuration.backend = Some(input.backend); - configuration.boundary_revision = session - .configuration - .boundary_revision - .checked_add(1) - .filter(|v| *v <= 9_007_199_254_740_991) - .ok_or("agent_permission_revision_exhausted")?; + let permissions = input.permissions; + let backend = input.backend; let result = repository .commit_workspace_restore( request, - serde_json::to_value(configuration).map_err(|e| e.to_string())?, now(), + move |configuration: &mut StoredSessionConfiguration| { + configuration.workspace = binding.clone(); + configuration.permissions = permissions.clone(); + configuration.backend = Some(backend.clone()); + configuration.boundary_revision = configuration + .boundary_revision + .checked_add(1) + .filter(|v| *v <= 9_007_199_254_740_991) + .ok_or_else(|| { + StoreError::InvalidTransition( + "permission boundary revision exhausted".into(), + ) + })?; + Ok(()) + }, ) .await .map(restored) diff --git a/plugins/agent/native/src/owner/checkpoints/session.rs b/plugins/agent/native/src/owner/checkpoints/session.rs index d008fa9fe62..be222e9dd52 100644 --- a/plugins/agent/native/src/owner/checkpoints/session.rs +++ b/plugins/agent/native/src/owner/checkpoints/session.rs @@ -86,7 +86,9 @@ impl Owner { .map(|value| response(value, None)) .map_err(failure); }; - let (admission, session) = owner.checkpoint_admission(&capture.session_id).await?; + let (admission, session) = owner + .checkpoint_admission(&capture.session_id, None) + .await?; if owner.behavior.is_some() || session.configuration.parent_session_id.is_some() || session @@ -94,6 +96,16 @@ impl Owner { .backend .as_ref() .is_some_and(|v| !matches!(v, BackendPreference::Model { .. })) + || owner.graph_selected(&session.id).await? + || owner + .store + .session(&session.id) + .await + .map_err(|e| e.to_string())? + .session_manager(&session.id) + .await + .map_err(|_| "agent_session_unavailable")? + .is_some() { return Err("agent_checkpoint_unsupported_session".into()); } diff --git a/plugins/agent/native/src/owner/compaction.rs b/plugins/agent/native/src/owner/compaction.rs index 0058fd1b2fd..bf39c6d6399 100644 --- a/plugins/agent/native/src/owner/compaction.rs +++ b/plugins/agent/native/src/owner/compaction.rs @@ -58,7 +58,17 @@ impl Owner { &self, input: SetCompactionPrompt, ) -> Result { + if self.runs.checkpointed(&input.session_id) { + return Ok(MetadataResult::Rejected { + reason: Rejection::Busy, + }); + } let runs = self.runs.lock().await; + if self.runs.checkpointed(&input.session_id) { + return Ok(MetadataResult::Rejected { + reason: Rejection::Busy, + }); + } if runs.closing { return Ok(MetadataResult::Rejected { reason: Rejection::Closed, @@ -104,7 +114,7 @@ impl Owner { operation: &maka_plugin_core::operation::RecoveryRef, ) -> Result { let mut runs = self.runs.lock().await; - let session = self.session(&input.session_id).await?; + let session = self.mutable_session(&input.session_id).await?; let identity = self.operation_id(operation); let invocation = Invocation { session_id: input.session_id.clone(), diff --git a/plugins/agent/native/src/owner/execution.rs b/plugins/agent/native/src/owner/execution.rs index af0981bda17..10b78a72f0b 100644 --- a/plugins/agent/native/src/owner/execution.rs +++ b/plugins/agent/native/src/owner/execution.rs @@ -397,6 +397,12 @@ impl Owner { .session(&source.session_id) .await .map_err(RunError::Internal)?; + if !self.runs.workspace_available( + &source.session_id, + std::path::Path::new(&session.configuration.workspace.path), + ) { + return Ok(()); + } drop(runs); let store = self .store @@ -446,6 +452,10 @@ impl Owner { return Err(RunError::Cancelled); } if runs.busy(&source.session_id) + || !self.runs.workspace_available( + &source.session_id, + std::path::Path::new(¤t.configuration.workspace.path), + ) || self .has_parked_invocation(&source.session_id) .await @@ -524,6 +534,10 @@ impl Owner { return Ok(ResumePreparation::Unavailable(Reason::Archived)); } if runs.busy(&input.session_id) + || !self.runs.workspace_available( + &input.session_id, + std::path::Path::new(&session.configuration.workspace.path), + ) || self.has_parked_invocation(&input.session_id).await? || !self .store diff --git a/plugins/agent/native/src/owner/goal.rs b/plugins/agent/native/src/owner/goal.rs index d5f04222e46..d72edb3a425 100644 --- a/plugins/agent/native/src/owner/goal.rs +++ b/plugins/agent/native/src/owner/goal.rs @@ -277,6 +277,15 @@ impl Owner { if runs.busy(session) { return Ok(false); } + if self.runs.has_checkpoints() { + let current = self.session(session).await?; + if !self.runs.workspace_available( + session, + std::path::Path::new(¤t.configuration.workspace.path), + ) { + return Ok(false); + } + } if !self .store .session(session) @@ -315,7 +324,7 @@ impl Owner { } }; let admission = self.goals.admissions.ordered(session).await; - self.session(session).await?; + self.mutable_session(session).await?; let runs = self.runs.lock().await; if runs.closing || self.stop.is_cancelled() { return Err("agent_closed".into()); diff --git a/plugins/agent/native/src/owner/graph.rs b/plugins/agent/native/src/owner/graph.rs index a526d781a77..36c8d1bded2 100644 --- a/plugins/agent/native/src/owner/graph.rs +++ b/plugins/agent/native/src/owner/graph.rs @@ -332,7 +332,7 @@ impl Owner { if runs.closing || owner.stop.is_cancelled() { return Err("agent_closed".into()); } - let session = owner.session(&input.session_id).await?; + let session = owner.mutable_session(&input.session_id).await?; let current = graphs.current(&input.session_id).await?; if current.as_ref().map(|c| &c.epoch.graph_id) != input.expected_graph_id.as_ref() { return graphs diff --git a/plugins/agent/native/src/owner/notifications.rs b/plugins/agent/native/src/owner/notifications.rs index e6fe4c05559..3c4659fe796 100644 --- a/plugins/agent/native/src/owner/notifications.rs +++ b/plugins/agent/native/src/owner/notifications.rs @@ -103,6 +103,10 @@ impl Owner { Err(error) => return Err(error), }; if session.archived + || !self.runs.workspace_available( + &source.session_id, + std::path::Path::new(&session.configuration.workspace.path), + ) || session.configuration.workspace != route.workspace || session.configuration.binding != route.binding { @@ -133,6 +137,10 @@ impl Owner { } let current = self.session(&source.session_id).await?; if runs.busy(&source.session_id) + || !self.runs.workspace_available( + &source.session_id, + std::path::Path::new(¤t.configuration.workspace.path), + ) || self.has_parked_invocation(&source.session_id).await? || current.configuration != session.configuration || current.archived diff --git a/plugins/agent/native/src/owner/patches.rs b/plugins/agent/native/src/owner/patches.rs index c32b840edd3..13e74d64eb7 100644 --- a/plugins/agent/native/src/owner/patches.rs +++ b/plugins/agent/native/src/owner/patches.rs @@ -131,7 +131,12 @@ impl Owner { if let Some(patch) = self.patch_record(&execution, binding.base_commit()).await? { return Ok(PatchResult::Ready { patch }); } - if runs.closing || !ended(&execution.state) { + if runs.closing + || !ended(&execution.state) + || !self + .runs + .workspace_available(&session.id, std::path::Path::new(&workspace.path)) + { return Ok(PatchResult::Busy); } if self @@ -210,6 +215,9 @@ impl Owner { if !self.owns(&member.session_id) || member.orphaned_shells || runs.busy(&member.session_id) + || !self + .runs + .workspace_available(&member.session_id, std::path::Path::new(path)) || self .store .session(&member.session_id) diff --git a/plugins/agent/native/src/owner/queue.rs b/plugins/agent/native/src/owner/queue.rs index 99b9f3f968b..84bf6da9191 100644 --- a/plugins/agent/native/src/owner/queue.rs +++ b/plugins/agent/native/src/owner/queue.rs @@ -53,7 +53,7 @@ impl maka_agent_engine::NativeApprovalExecutor for Owner { > { Box::pin(async move { let session = self - .session(&pending.invocation.session_id) + .mutable_session(&pending.invocation.session_id) .await .map_err(maka_agent_facts::tools::ToolError::Failed)?; workspace::validate( @@ -158,6 +158,15 @@ impl Owner { if !self.owns(&session) { continue; } + if self.runs.has_checkpoints() + && let Ok(current) = self.session(&session).await + && !self.runs.workspace_available( + &session, + std::path::Path::new(¤t.configuration.workspace.path), + ) + { + continue; + } let mut runs = self.runs.state().await; if runs.closing || self.stop.is_cancelled() { return Ok(()); @@ -192,7 +201,7 @@ impl Owner { } // Recover the owner-scoped durable backlog without limiting active Sessions. - async fn schedule(self: &Arc) { + pub(super) async fn schedule(self: &Arc) { if let Err(error) = self.schedule_pending().await && !(self.stop.is_cancelled() && matches!( @@ -236,6 +245,15 @@ impl Owner { { continue; } + if self.runs.has_checkpoints() + && let Ok(current) = self.session(session).await + && !self.runs.workspace_available( + session, + std::path::Path::new(¤t.configuration.workspace.path), + ) + { + continue; + } let mut runs = self.runs.state().await; if runs.closing || self.stop.is_cancelled() { return Ok(()); @@ -460,6 +478,23 @@ impl Owner { if cancellation.is_cancelled() { return Ok(None); } + if self.runs.has_checkpoints() { + if self.runs.checkpointed(session) { + return Ok(None); + } + if let Some(record) = self + .store + .get_session::(session) + .await? + && let Ok(resolved) = resolve_session(record) + && !self.runs.workspace_available( + session, + std::path::Path::new(&resolved.configuration.workspace.path), + ) + { + return Ok(None); + } + } let store = self.store.session(session).await?; if let Some(checkpoint) = store.activation_checkpoint(session).await? { return match &checkpoint.checkpoint.suspension { diff --git a/plugins/agent/native/src/owner/runs.rs b/plugins/agent/native/src/owner/runs.rs index ba8d1ff37dd..45d74ad17e0 100644 --- a/plugins/agent/native/src/owner/runs.rs +++ b/plugins/agent/native/src/owner/runs.rs @@ -35,7 +35,7 @@ pub(super) struct Registry { activity: Arc>, state: Arc>, sessions: MapMutex>>, - checkpoints: Arc>>, + checkpoints: Arc>>>, } impl Default for Registry { @@ -58,7 +58,7 @@ pub(super) fn overlaps(a: &Path, b: &Path) -> bool { } pub(super) struct Checkpoint { - checkpoints: Arc>>, + checkpoints: Arc>>>, session: String, } impl Drop for Checkpoint { @@ -77,24 +77,33 @@ impl Registry { .map(|(id, _)| id.clone()) .collect() } + pub(super) fn has_checkpoints(&self) -> bool { + !self.checkpoints.lock().unwrap().is_empty() + } pub(super) fn checkpointed(&self, session: &str) -> bool { self.checkpoints.lock().unwrap().contains_key(session) } pub(super) fn workspace_available(&self, session: &str, path: &Path) -> bool { - self.checkpoints - .lock() - .unwrap() - .iter() - .all(|(id, active)| id != session && !overlaps(path, active)) + self.checkpoints.lock().unwrap().iter().all(|(id, active)| { + id != session && active.iter().all(|reserved| !overlaps(path, reserved)) + }) } - pub(super) fn checkpoint(&self, session: &str, path: PathBuf) -> Result { + pub(super) fn checkpoint( + &self, + session: &str, + paths: Vec, + ) -> Result { let mut checkpoints = self.checkpoints.lock().unwrap(); if checkpoints.contains_key(session) - || checkpoints.values().any(|active| overlaps(&path, active)) + || checkpoints.values().any(|active| { + active + .iter() + .any(|reserved| paths.iter().any(|path| overlaps(path, reserved))) + }) { return Err("agent_checkpoint_busy".into()); } - checkpoints.insert(session.to_owned(), path); + checkpoints.insert(session.to_owned(), paths); Ok(Checkpoint { checkpoints: self.checkpoints.clone(), session: session.to_owned(), diff --git a/plugins/agent/native/src/owner/scheduler/dispatch.rs b/plugins/agent/native/src/owner/scheduler/dispatch.rs index c628ae79e4c..0336c3235ca 100644 --- a/plugins/agent/native/src/owner/scheduler/dispatch.rs +++ b/plugins/agent/native/src/owner/scheduler/dispatch.rs @@ -112,7 +112,7 @@ impl Owner { if self.graph_selected(id).await? { return Err("graph_background_conflict".into()); } - let session = self.session(id).await?; + let session = self.mutable_session(id).await?; if session.configuration.workspace != target.workspace || session.configuration.permissions != target.permissions || session.configuration.boundary_revision != target.boundary_revision @@ -155,7 +155,7 @@ impl Owner { if runs.closing || self.stop.is_cancelled() { return Err("agent_closed".into()); } - let current = self.session(id).await?; + let current = self.mutable_session(id).await?; if current.configuration != session.configuration || current.archived { return Err("scheduler_authorization_changed".into()); } diff --git a/plugins/agent/native/src/owner/tests.rs b/plugins/agent/native/src/owner/tests.rs index 1becff2640b..26e536dbdb5 100644 --- a/plugins/agent/native/src/owner/tests.rs +++ b/plugins/agent/native/src/owner/tests.rs @@ -534,6 +534,22 @@ impl Fixture { ) -> Self { Self::configured_provider(base, model, peer, tooling, configure, "openai-compatible").await } + async fn configured_with_temp_home( + base: &str, + configure: impl FnOnce(&mut Configuration), + ) -> Self { + Self::configured_application( + base, + None, + None, + None, + configure, + "openai-compatible", + FixtureApplication::Agent, + true, + ) + .await + } async fn configured_provider( base: &str, model: Option, @@ -550,6 +566,7 @@ impl Fixture { configure, provider_name, FixtureApplication::Agent, + false, ) .await } @@ -569,9 +586,11 @@ impl Fixture { configure, "openai-compatible", FixtureApplication::Workhub(setup), + false, ) .await } + #[allow(clippy::too_many_arguments)] async fn configured_application( base: &str, model: Option, @@ -580,6 +599,7 @@ impl Fixture { configure: impl FnOnce(&mut Configuration), provider_name: &str, application: FixtureApplication, + use_temp_home: bool, ) -> Self { let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("root"); @@ -708,6 +728,7 @@ impl Fixture { FixtureApplication::Agent => { let capture = captured.clone(); let replacement = Arc::new(StdMutex::new(model)); + let temp_home = use_temp_home.then(|| dir.path().to_path_buf()); // Actual Host NativeContext and original NativeOwner; only the model // implementation is controlled in failure tests, never Root/admission. native @@ -717,10 +738,14 @@ impl Fixture { let capture = capture.clone(); let catalog = catalog.clone(); let replacement = replacement.lock().unwrap().take(); + let temp_home = temp_home.clone(); Box::pin(async move { let services = context.services(); let mut owner = Owner::open(context, configuration, catalog).await.unwrap(); + if let Some(home) = temp_home { + Arc::get_mut(&mut owner).unwrap().home = Some(home); + } if let Some(model) = replacement { Arc::get_mut(&mut owner).unwrap().engine = Engine::new( model, diff --git a/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs b/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs index 3c445015cdd..9e7a40e0068 100644 --- a/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs +++ b/plugins/agent/native/src/owner/tests/checkpoints/workspace.rs @@ -228,15 +228,40 @@ async fn checkpoint_admission_excludes_shared_workspaces_without_stalling_other_ .unwrap(); let b = f.mutation(2,"create",json!({"title":"Unrelated B","labels":[],"workspace":{"kind":"directory","path":roots["roots"][1]["path"]}}),Uuid::new_v4()).await.unwrap()["session"]["id"].as_str().unwrap().to_owned(); let shared_producer = f.owner.runs.ordered(&shared).await; - assert!(f.owner.checkpoint_admission(&a).await.is_err()); + assert!(f.owner.checkpoint_admission(&a, None).await.is_err()); drop(shared_producer); let producer_b = f.owner.goals.admissions.ordered(&b).await; - let (admission, _) = f.owner.checkpoint_admission(&a).await.unwrap(); - assert!(f.owner.session(&a).await.is_err()); - assert!(f.owner.session(&shared).await.is_err()); - assert!(f.owner.checkpoint_admission(&shared).await.is_err()); + let (admission, _) = f.owner.checkpoint_admission(&a, None).await.unwrap(); + // Read-only session/history observation remains available while mutations are excluded. + assert!(f.query("observe", json!({"sessionId":a})).await.is_ok()); + assert!( + f.query("observe", json!({"sessionId":shared})) + .await + .is_ok() + ); + assert!( + f.query( + "history", + json!({"sessionId":shared,"position":{"kind":"tail"}}), + ) + .await + .is_ok() + ); + assert!(f.send(&shared).await.is_err()); + assert_eq!( + f.mutation( + 2, + "metadata", + json!({"sessionId":a,"expectedRevision":1,"title":"Busy","labels":[]}), + Uuid::new_v4(), + ) + .await + .unwrap()["reason"], + "busy" + ); + assert!(f.owner.checkpoint_admission(&shared, None).await.is_err()); drop(producer_b); - let (other_checkpoint, _) = f.owner.checkpoint_admission(&b).await.unwrap(); + let (other_checkpoint, _) = f.owner.checkpoint_admission(&b, None).await.unwrap(); drop(other_checkpoint); tokio::time::timeout( Duration::from_secs(30), @@ -248,7 +273,7 @@ async fn checkpoint_admission_excludes_shared_workspaces_without_stalling_other_ let changed = f.mutation(2,"metadata",json!({"sessionId":b,"expectedRevision":current["session"]["revision"],"title":"B still writable","labels":[]}),Uuid::new_v4()).await.unwrap(); assert_eq!(changed["kind"], "committed"); drop(admission); - assert!(f.owner.session(&shared).await.is_ok()); + assert!(f.owner.mutable_session(&shared).await.is_ok()); // An admitted run in a distinct directory must not block A's capture. f.send(&b).await.unwrap(); let (stream, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) @@ -268,3 +293,210 @@ async fn checkpoint_admission_excludes_shared_workspaces_without_stalling_other_ drop(stream); close(&f).await; } + +#[tokio::test] +async fn active_goal_defers_during_shared_checkpoint_and_restore_enforces_tilde_and_target_parent_fencing() + { + tokio::time::timeout(Duration::from_secs(90), async { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = Fixture::configured_with_temp_home( + &format!("http://{}/v1", listener.local_addr().unwrap()), + |settings| { + settings.goals = true; + settings.tool_discovery = true; + }, + ) + .await; + let a = f.create("Checkpoint A").await; + let shared = f.create("Shared goal session").await; + let roots = f + .query("directories", json!({"kind":"roots"})) + .await + .unwrap(); + let b = f + .mutation( + 2, + "create", + json!({"title":"Target parent B","labels":[],"workspace":{"kind":"directory","path":roots["roots"][1]["path"]}}), + Uuid::new_v4(), + ) + .await + .unwrap()["session"]["id"] + .as_str() + .unwrap() + .to_owned(); + + // Arm a Goal on `shared` (start=false), hold checkpoint admission on `a`, then start `shared`'s Goal + // — wait: starting while `a` holds admission must reject `changeGoal` on `shared` as busy, + // and if `shared`'s Goal is already active while `shared`'s turn finishes into `a`'s checkpoint, + // the Goal tick must defer cleanly (never marking `authorityBlocked`) and resume once `a` releases! + f.send(&shared).await.unwrap(); + let (mut shared_turn, _) = listener.accept().await.unwrap(); + request(&mut shared_turn).await; + let shared_cfg = f.owner.session(&shared).await.unwrap().configuration; + let goal_approval = json!({ + "target":{ + "sessionId":shared, + "workspace":shared_cfg.workspace, + "permissions":shared_cfg.permissions, + "boundaryRevision":shared_cfg.boundary_revision, + "binding":shared_cfg.binding + }, + "model":f.selection().await, + "thinkingLevel":null + }); + let created_goal = f + .mutation( + 2, + "changeGoal", + json!({"kind":"create","sessionId":shared,"objective":"Finish after checkpoint","start":true,"tokenBudget":null,"approval":goal_approval}), + Uuid::new_v4(), + ) + .await + .unwrap(); + assert_eq!(created_goal["kind"], "committed"); + response_with_text(&mut shared_turn, "initial turn done").await; + // Accept the Goal's first continuation turn, pause before completing, finish it, then hold `a`'s checkpoint: + let (mut goal_turn_1, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + request(&mut goal_turn_1).await; + response_with_text(&mut goal_turn_1, "working on goal step 1").await; + // Wait for `shared` to settle idle or next goal tick; grab admission on `a` as soon as `shared` is between turns, + // OR test holding `admission` on `a` when `Goals::execution_changed()` fires! + let (mut goal_turn_2, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + request(&mut goal_turn_2).await; + // Manually insert `a` into `runs.checkpoints` to simulate `a` holding workspace checkpoint right as `goal_turn_2` finishes! + let held = f + .owner + .runs + .checkpoint(&a, vec![f.workspace.clone()]) + .unwrap(); + response_with_text(&mut goal_turn_2, "working on goal step 2").await; + f.drained().await; + f.owner.goals.execution_changed(); + tokio::time::sleep(Duration::from_millis(120)).await; + let during = f.query("goal", json!({"sessionId":shared})).await.unwrap()["current"].clone(); + assert_eq!( + during["status"], "active", + "Goal must defer instead of failing: {during}" + ); + assert_eq!(during["note"], ""); + assert!( + !f.owner + .goals + .repository + .read(&shared) + .await + .unwrap() + .unwrap() + .goal + .authority_blocked + ); + drop(held); + f.owner.goals.execution_changed(); + let (mut goal_turn_3, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + let sent = request(&mut goal_turn_3).await; + let has_exec = sent["tools"] + .as_array() + .unwrap() + .iter() + .any(|tool| tool["function"]["name"] == "exec"); + let (tool_name, tool_input) = if has_exec { + ( + "exec", + json!({"code":"text(await tools.GoalStatus({\"status\":\"achieved\",\"note\":\"done after checkpoint\"}));"}), + ) + } else { + ( + "GoalStatus", + json!({"status":"achieved","note":"done after checkpoint"}), + ) + }; + service_tools::named_tool_response(&mut goal_turn_3, tool_name, tool_input).await; + let (mut goal_finish, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + request(&mut goal_finish).await; + response_with_text(&mut goal_finish, "goal completed").await; + f.drained().await; + + // Publish checkpoint on `a`, then verify non-Remote scope rejection, busy target `parent` rejection, and `~/` parent expansion. + std::fs::write(f.workspace.join("tilde.txt"), "restored via tilde").unwrap(); + let published = f + .mutation( + 2, + "publishCheckpoint", + workspace(input(&f, &a, Value::Null).await, "unverified"), + Uuid::new_v4(), + ) + .await + .unwrap(); + let revision = published["state"]["head"]["revision"].clone(); + let bg_scope = f + .issuer + .issue_with( + Identity::Background { + grant: maka_capabilities::authorization::Id(Uuid::new_v4()), + }, + true, + CancellationToken::new(), + ) + .unwrap(); + assert!( + f.owner + .preview_history_checkpoint( + contract::SessionRead { + session_id: a.clone() + }, + &bg_scope, + ) + .await + .is_err() + ); + + let restore_tilde = json!({ + "checkpoint":{"sessionId":a,"revision":revision}, + "parent":"~/other-workspace", + "permissions":{"access":"workspace_write","requestPermissions":true}, + "backend":model_backend(&f.selection().await), + "acceptExternalWritersRisk":true + }); + // Active run in `~/other-workspace` (`b`) must block restoring `a` into `~/other-workspace`. + f.send(&b).await.unwrap(); + let (mut b_socket, _) = tokio::time::timeout(Duration::from_secs(15), listener.accept()) + .await + .unwrap() + .unwrap(); + assert!( + f.mutation(2, "restoreSessionCheckpoint", restore_tilde.clone(), Uuid::new_v4()) + .await + .is_err() + ); + request(&mut b_socket).await; + response_with_text(&mut b_socket, "b finished").await; + f.drained().await; + + let restored = f + .mutation(2, "restoreSessionCheckpoint", restore_tilde, Uuid::new_v4()) + .await + .unwrap(); + let restored_path = std::path::PathBuf::from(restored["path"].as_str().unwrap()); + assert!(restored_path.starts_with(roots["roots"][1]["path"].as_str().unwrap())); + assert_eq!( + std::fs::read_to_string(restored_path.join("tilde.txt")).unwrap(), + "restored via tilde" + ); + close(&f).await; + }) + .await + .unwrap(); +}