zigmini 1.0.0-beta.3 → 1.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.ko.md +27 -2
  2. package/README.md +27 -2
  3. package/assets/agent/gateway-request-rules.md +1 -1
  4. package/assets/agent/gateway-rules.md +6 -1
  5. package/assets/android/zigmini.apk +0 -0
  6. package/assets/android/zigmini.apk.json +3 -3
  7. package/assets/build.json +1 -1
  8. package/assets/document-reader/document-reader +0 -0
  9. package/assets/document-reader/host-selector +0 -0
  10. package/assets/document-reader/manifest.json +27 -0
  11. package/docs/manual.ko.md +114 -10
  12. package/docs/manual.md +117 -11
  13. package/host/document-reader/DocumentRead.swift +370 -0
  14. package/host/document-reader/HostSelector.swift +192 -0
  15. package/host/document-reader/Protocol.swift +647 -0
  16. package/host/document-reader/main.swift +22 -0
  17. package/host/omo-bridge/agent-aside-tool.ts +119 -0
  18. package/host/omo-bridge/agent-aside.ts +522 -0
  19. package/host/omo-bridge/agent-collaboration-files.ts +282 -0
  20. package/host/omo-bridge/agent-collaboration.ts +385 -0
  21. package/host/omo-bridge/agent-control.ts +400 -28
  22. package/host/omo-bridge/agent-execution.sb +42 -0
  23. package/host/omo-bridge/agent-execution.ts +158 -0
  24. package/host/omo-bridge/agent-tool-adapters.ts +388 -0
  25. package/host/omo-bridge/agent-tool-permissions.ts +96 -0
  26. package/host/omo-bridge/aside-tabs.ts +41 -0
  27. package/host/omo-bridge/attachment-read.ts +100 -0
  28. package/host/omo-bridge/bridge.ts +10 -0
  29. package/host/omo-bridge/managed-call.ts +46 -0
  30. package/host/omo-bridge/task-control.ts +39 -5
  31. package/host/omo-check/patches/attachments-293/README.md +138 -0
  32. package/host/omo-check/patches/attachments-293/document-content.js +234 -0
  33. package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
  34. package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
  35. package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
  36. package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
  37. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +28 -6
  38. package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
  39. package/package.json +5 -3
  40. package/src/agent/api.ts +345 -20
  41. package/src/agent/artifacts.ts +196 -0
  42. package/src/agent/aside-target-routes.ts +369 -0
  43. package/src/agent/aside-targets.ts +215 -0
  44. package/src/agent/collaboration-admission.ts +47 -0
  45. package/src/agent/collaboration-read.ts +117 -0
  46. package/src/agent/collaboration-session.ts +196 -0
  47. package/src/agent/collaboration-types.ts +258 -0
  48. package/src/agent/collaboration.ts +399 -0
  49. package/src/agent/definition.ts +65 -3
  50. package/src/agent/fields.ts +9 -3
  51. package/src/agent/file-core.ts +325 -0
  52. package/src/agent/files.ts +87 -8
  53. package/src/agent/list.ts +3 -3
  54. package/src/agent/managed-call.ts +144 -0
  55. package/src/agent/tool-catalog.ts +184 -0
  56. package/src/agent/tool-delivery.ts +359 -0
  57. package/src/agent/tool-permission-types.ts +81 -0
  58. package/src/agent/tool-permissions.ts +1366 -0
  59. package/src/attachments/api.ts +161 -0
  60. package/src/attachments/document-reader.ts +325 -0
  61. package/src/attachments/host-selector.ts +410 -0
  62. package/src/attachments/managed-binding.ts +83 -0
  63. package/src/attachments/prepare.ts +214 -0
  64. package/src/attachments/selector-port.ts +28 -0
  65. package/src/attachments/service.ts +729 -0
  66. package/src/attachments/store.ts +698 -0
  67. package/src/attachments/types.ts +123 -0
  68. package/src/cli/doctor.ts +30 -10
  69. package/src/decisions.ts +109 -10
  70. package/src/engine/apply-patch.ts +21 -0
  71. package/src/engine/compat.json +2 -1
  72. package/src/engine/install.ts +45 -4
  73. package/src/engine/sandbox-cell.ts +2 -0
  74. package/src/gc.ts +33 -225
  75. package/src/housekeeping/api.ts +163 -0
  76. package/src/housekeeping/audit.ts +61 -0
  77. package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
  78. package/src/housekeeping/collectors/agent-drafts.ts +43 -0
  79. package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
  80. package/src/housekeeping/collectors/agent-trash.ts +28 -0
  81. package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
  82. package/src/housekeeping/collectors/db-backup.ts +25 -0
  83. package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
  84. package/src/housekeeping/collectors/docker-image.ts +135 -0
  85. package/src/housekeeping/collectors/omo-version.ts +32 -0
  86. package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
  87. package/src/housekeeping/collectors/reconcile-work.ts +55 -0
  88. package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
  89. package/src/housekeeping/collectors/shared.ts +149 -0
  90. package/src/housekeeping/collectors/temp-dir.ts +28 -0
  91. package/src/housekeeping/collectors/update-file.ts +25 -0
  92. package/src/housekeeping/collectors/worktree.ts +193 -0
  93. package/src/housekeeping/engine.ts +125 -0
  94. package/src/housekeeping/reconcile/work.ts +130 -0
  95. package/src/housekeeping/registry.ts +63 -0
  96. package/src/housekeeping/trash.ts +59 -0
  97. package/src/housekeeping/types.ts +31 -0
  98. package/src/hub.ts +1095 -41
  99. package/src/main.ts +228 -30
  100. package/src/message-references.ts +14 -5
  101. package/src/profile-prompt.ts +13 -1
  102. package/src/protocol.ts +41 -6
  103. package/src/security-allow.ts +79 -2
  104. package/src/security-check.ts +1 -1
  105. package/src/store.ts +1946 -23
  106. package/src/test-crash.ts +20 -0
  107. package/src/work-watch.ts +55 -7
  108. package/src/work.ts +192 -3
  109. package/web/DESIGN.md +33 -0
  110. package/web/agents.d.ts +189 -12
  111. package/web/agents.js +898 -39
  112. package/web/app.js +2634 -210
  113. package/web/attachments.d.ts +91 -0
  114. package/web/attachments.js +422 -0
  115. package/web/collaboration.d.ts +87 -0
  116. package/web/collaboration.js +290 -0
  117. package/web/decisions.d.ts +39 -2
  118. package/web/decisions.js +354 -2
  119. package/web/gc.d.ts +26 -5
  120. package/web/gc.js +113 -13
  121. package/web/i18n/agents.d.ts +4 -0
  122. package/web/i18n/agents.js +200 -8
  123. package/web/i18n/attachments.js +78 -0
  124. package/web/i18n/decisions.d.ts +4 -0
  125. package/web/i18n/decisions.js +68 -0
  126. package/web/i18n/gc.d.ts +4 -0
  127. package/web/i18n/gc.js +184 -2
  128. package/web/i18n/main.js +80 -8
  129. package/web/i18n/tour.d.ts +4 -0
  130. package/web/i18n/tour.js +37 -0
  131. package/web/index.html +20 -3
  132. package/web/profile.d.ts +2 -2
  133. package/web/profile.js +11 -6
  134. package/web/style.css +342 -21
  135. package/web/tooltip.d.ts +2 -0
  136. package/web/tooltip.js +12 -0
  137. package/web/tour.d.ts +57 -0
  138. package/web/tour.js +450 -0
@@ -0,0 +1,258 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Database } from "bun:sqlite";
3
+
4
+ export type AgentRequestState = "queued" | "running" | "waiting_peer" | "waiting_owner" | "result_preparing" |
5
+ "completed" | "failed" | "interrupted" | "cancel_requested" | "cancelled" | "unresolved_delivery" | "version_conflict";
6
+ export type AgentDelivery = "queued" | "attempted" | "acknowledged" | "consumed" | "rejected" | "unknown";
7
+ export type AgentStageState = "awaiting_transfer_authority" | "ready" | "staging" | "staged" | "denied" | "cancelled" | "invalidated" | "unknown";
8
+ export interface CollaborationBinding {
9
+ readonly convId: string;
10
+ readonly agentId: string | null;
11
+ readonly lifecycleEpoch: number;
12
+ readonly inputId: string;
13
+ readonly activationId: string | null;
14
+ readonly sessionPath: string;
15
+ readonly durableSessionId: string;
16
+ readonly gatewayGeneration: string;
17
+ readonly definitionSha: string;
18
+ readonly ownerInputRevision: number;
19
+ readonly readContinuationId?: string;
20
+ }
21
+ export interface CollaborationTarget {
22
+ readonly agentId: string;
23
+ readonly convId: string;
24
+ readonly lifecycleEpoch: number;
25
+ }
26
+ export interface AgentAccessView {
27
+ readonly agentId: string;
28
+ readonly lifecycleEpoch: number;
29
+ readonly accessState: "open" | "blocked";
30
+ readonly authorityEpoch: number;
31
+ readonly policyRevision: number;
32
+ }
33
+ /** Synchronous providers run under the Store writer transaction, never across HTTP/helper waits. */
34
+ export type CollaborationGate = {
35
+ readonly assertAccess: (tx: Database, source: CollaborationBinding, target: CollaborationTarget) => undefined;
36
+ readonly assertSelectedExportEligible?: (tx: Database, receipt: AttachmentOrigin) => undefined;
37
+ readonly assertTransferAuthority?: (tx: Database, stage: AgentArtifactStage) => undefined;
38
+ } & (
39
+ | { readonly mode: "standalone" }
40
+ | { readonly mode: "authority"; readonly access: (tx: Database, agentId: string) => AgentAccessView;
41
+ readonly permissionOutcome?: (tx: Database, decisionId: string) => CollaborationPermissionOutcome | null }
42
+ );
43
+ export interface AgentRequestInput {
44
+ readonly toolCallId: string;
45
+ readonly targetAgentId: string;
46
+ readonly kind: "task" | "question";
47
+ readonly text: string;
48
+ readonly files: readonly string[];
49
+ readonly retryOf?: string;
50
+ }
51
+ export interface AgentRequest {
52
+ readonly id: string;
53
+ readonly rootId: string;
54
+ readonly parentId: string | null;
55
+ readonly requesterConv: string;
56
+ readonly requesterAgent: string | null;
57
+ readonly targetConv: string;
58
+ readonly targetAgent: string;
59
+ readonly requesterLifecycleEpoch: number;
60
+ readonly targetLifecycleEpoch: number;
61
+ readonly requesterAuthorityEpoch?: number;
62
+ readonly recipientAuthorityEpoch?: number;
63
+ readonly kind: AgentRequestInput["kind"];
64
+ readonly payload: AgentRequestInput;
65
+ readonly payloadSha: string;
66
+ readonly submissionKey: string;
67
+ readonly fingerprint: string;
68
+ readonly state: AgentRequestState;
69
+ readonly revision: number;
70
+ readonly createdAt: string;
71
+ readonly resultId: string | null;
72
+ readonly cancelReason: string | null;
73
+ }
74
+ export interface AgentActivation {
75
+ readonly id: string;
76
+ readonly requestId: string;
77
+ readonly direction: "request" | "reply" | "owner_answer";
78
+ readonly ordinal: number;
79
+ readonly inputId: string;
80
+ readonly inputSeq: number;
81
+ readonly targetConv: string;
82
+ readonly binding: CollaborationBinding | null;
83
+ readonly state: AgentDelivery;
84
+ readonly revision: number;
85
+ readonly consumedEntry: string | null;
86
+ readonly quiesced: boolean;
87
+ }
88
+ export interface AgentReceipt {
89
+ readonly requestId: string;
90
+ readonly state: AgentRequestState;
91
+ readonly revision: number;
92
+ }
93
+ /** A cancel receipt also reports the state of every descendant request after the cancel. */
94
+ export interface AgentCancelReceipt extends AgentReceipt {
95
+ readonly children: readonly AgentReceipt[];
96
+ }
97
+ export interface AgentResultInput {
98
+ readonly requestId: string;
99
+ readonly expectedRevision: number;
100
+ readonly toolCallId: string;
101
+ readonly outcome: "completed" | "failed";
102
+ readonly text: string;
103
+ readonly artifacts: readonly string[];
104
+ }
105
+ export interface AgentResult {
106
+ readonly id: string;
107
+ readonly requestId: string;
108
+ readonly activationId: string;
109
+ readonly submissionKey: string;
110
+ readonly payloadSha: string;
111
+ readonly text: string;
112
+ readonly outcome: "completed" | "failed";
113
+ readonly manifest: readonly AgentArtifact[];
114
+ readonly committedAt: string;
115
+ }
116
+ export interface AttachmentOrigin {
117
+ readonly attachmentId: string;
118
+ readonly sourceGrantId: string;
119
+ readonly sourceGrantRevision: number;
120
+ readonly originMessageSeq: number;
121
+ readonly sourceConvId: string;
122
+ readonly sourceRecipientId: string;
123
+ readonly sourceConvEpoch: number;
124
+ readonly sourceRecipientEpoch: number;
125
+ readonly originalSha256: string;
126
+ readonly bytes: number;
127
+ readonly format: string;
128
+ readonly name: string;
129
+ }
130
+ export type TransferAuthority =
131
+ | { readonly kind: "owner_decision"; readonly decisionId: string; readonly decisionRevision: number; readonly submissionId: string; readonly scopeDigest: string; readonly transferId: string }
132
+ | { readonly kind: "tool_policy"; readonly authorityId: string; readonly revision: number; readonly policyRevision: number; readonly authorityEpoch: number; readonly scopeDigest: string; readonly actionTicketId?: string };
133
+ export type StageSource =
134
+ | { readonly kind: "agent_path"; readonly path: string; readonly sha256: string }
135
+ | { readonly kind: "attachment"; readonly origin: AttachmentOrigin }
136
+ | { readonly kind: "artifact"; readonly requestId: string; readonly artifactId: string; readonly grantId: string; readonly grantRevision: number; readonly sha256: string };
137
+ export interface AgentStageInput {
138
+ readonly transferId?: string;
139
+ readonly operationKey: string;
140
+ readonly source: StageSource;
141
+ readonly target: CollaborationTarget;
142
+ readonly ownerPrincipalRef: string;
143
+ readonly authority: TransferAuthority | null;
144
+ }
145
+ export interface AgentArtifactStage extends AgentStageInput {
146
+ readonly transferId: string;
147
+ readonly payloadSha: string;
148
+ readonly scopeDigest: string;
149
+ readonly sourceBinding: CollaborationBinding;
150
+ readonly sourceAuthorityEpoch?: number;
151
+ readonly targetAuthorityEpoch?: number;
152
+ readonly originRequestId: string | null;
153
+ readonly decisionId: string | null;
154
+ readonly decisionRevision: number | null;
155
+ readonly continuationId: string;
156
+ readonly state: AgentStageState;
157
+ readonly revision: number;
158
+ readonly artifactId: string | null;
159
+ readonly exportReceipt: AttachmentOrigin | null;
160
+ readonly errorCode: string | null;
161
+ }
162
+ export interface ArtifactProvenance {
163
+ readonly transferId: string | null;
164
+ readonly originAttachment?: AttachmentOrigin;
165
+ readonly sourceArtifactId?: string;
166
+ readonly sourceArtifactGrantRevision?: number;
167
+ readonly parentTransferId?: string;
168
+ readonly transferAuthorityRef: TransferAuthority | null;
169
+ readonly targetAgentId: string;
170
+ readonly targetConvId: string;
171
+ readonly targetLifecycleEpoch: number;
172
+ readonly sourceInputId: string;
173
+ readonly sourceActivationId: string | null;
174
+ readonly requestId: string | null;
175
+ readonly rootId: string | null;
176
+ readonly producedBy?: { readonly requestId: string; readonly activationId: string; readonly producerConv: string };
177
+ readonly inputArtifactIds?: readonly string[];
178
+ }
179
+ export interface AgentArtifact {
180
+ readonly id: string;
181
+ readonly producerConv: string;
182
+ readonly requestId: string | null;
183
+ readonly resultId?: string;
184
+ readonly sha256: string;
185
+ readonly bytes: number;
186
+ readonly mediaType: string;
187
+ readonly originalName: string;
188
+ readonly sealedName: string;
189
+ readonly state: "staged" | "committed" | "trashed";
190
+ readonly lifecycleEpoch: number;
191
+ readonly storageClass: "source_copy" | "scoped_document";
192
+ readonly stageOperationKey: string;
193
+ readonly stagePayloadSha: string;
194
+ readonly provenance: ArtifactProvenance;
195
+ }
196
+ export interface ArtifactGrant {
197
+ readonly grantId: string;
198
+ readonly artifactId: string;
199
+ readonly granteeConv: string;
200
+ readonly requestId: string;
201
+ readonly permission: "read" | "transfer";
202
+ readonly revoked: boolean;
203
+ readonly revision: number;
204
+ readonly granteeLifecycleEpoch: number;
205
+ readonly recipientAuthorityEpoch?: number;
206
+ }
207
+ export interface CollaborationPermissionOutcome {
208
+ readonly decisionId: string;
209
+ readonly decisionRevision: number;
210
+ readonly outcome: "allowed" | "denied" | "cancelled";
211
+ readonly logicalOperationId: string;
212
+ readonly continuationId: string;
213
+ readonly collaborationRequestId: string;
214
+ readonly blockedActivationId: string;
215
+ readonly originInputSeq: number;
216
+ readonly lifecycleEpoch: number;
217
+ readonly authorityEpoch: number;
218
+ readonly policyRevision: number;
219
+ }
220
+ export type CollaborationIntent =
221
+ | { readonly origin: "agent_collaboration"; readonly requestId: string; readonly activationId: string; readonly direction: AgentActivation["direction"]; readonly messageSeq: number }
222
+ | { readonly origin: "collaboration_stage"; readonly transferId: string; readonly continuationId: string }
223
+ | { readonly origin: "agent_tool_permission"; readonly outcome: CollaborationPermissionOutcome }
224
+ | { readonly origin: "collaboration_artifact_read"; readonly holdId: string; readonly readOperationId: string; readonly continuationId: string; readonly reviewedScopeDigest: string };
225
+
226
+ export class CollaborationStoreError extends Error {
227
+ constructor(readonly code: string, readonly status: 400 | 403 | 404 | 409 | 410 | 413 | 503 = 409) { super(code); }
228
+ }
229
+ export function collaborationDigest(value: unknown): string {
230
+ // All callers provide named internal DTOs; recursively sort keys without semantic text normalization.
231
+ const canonical = (input: unknown): unknown => {
232
+ if (Array.isArray(input)) return input.map(canonical);
233
+ if (typeof input !== "object" || input === null) return input;
234
+ return Object.fromEntries(Object.entries(input).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
235
+ };
236
+ return createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex");
237
+ }
238
+ /**
239
+ * Final request states, defined once: nothing re-runs them automatically and no later transition rewrites them.
240
+ * unresolved_delivery is not final: only an evidence recheck resolves it, and it is never retried automatically.
241
+ */
242
+ export const AGENT_REQUEST_FINAL_STATES = ["completed", "failed", "cancelled", "interrupted", "version_conflict"] as const satisfies readonly AgentRequestState[];
243
+ /** The final states as a SQL list literal for `state IN ...` / `state NOT IN ...`, including the partial unique index. */
244
+ export const AGENT_REQUEST_FINAL_SQL = `(${AGENT_REQUEST_FINAL_STATES.map(state => `'${state}'`).join(",")})`;
245
+ export function agentRequestTerminal(state: AgentRequestState): boolean {
246
+ return (AGENT_REQUEST_FINAL_STATES as readonly AgentRequestState[]).includes(state);
247
+ }
248
+ /** A final original that only an explicit main/operator retryOf may follow with a new request; completed is never retried. */
249
+ export function agentRequestRetryable(state: AgentRequestState): boolean {
250
+ return agentRequestTerminal(state) && state !== "completed";
251
+ }
252
+ export function collaborationNever(value: never): never {
253
+ throw new CollaborationStoreError("invalid_variant", 400);
254
+ }
255
+ export function collaborationStageDecision(decision: { readonly payload: unknown }): boolean {
256
+ const payload = decision.payload;
257
+ return typeof payload === "object" && payload !== null && "purpose" in payload && payload.purpose === "collaboration_attachment_transfer";
258
+ }
@@ -0,0 +1,399 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import type { Hub } from "../hub.ts";
4
+ import { maintenanceAuth } from "../maintenance.ts";
5
+ import { OmoCommandError } from "../omo/client.ts";
6
+ import type { Store } from "../store.ts";
7
+ import { join } from "node:path";
8
+ import { AgentArtifacts } from "./artifacts.ts";
9
+ import { AgentContentError, AgentContentFiles } from "./file-core.ts";
10
+ import type { AgentFileStore } from "./files.ts";
11
+ import { ManagedCallError } from "./managed-call.ts";
12
+ import { CollaborationStoreError, collaborationDigest, collaborationNever, type AgentArtifact, type CollaborationGate } from "./collaboration-types.ts";
13
+ import { CollaborationSessions, collaborationRecord as record, collaborationText as text } from "./collaboration-session.ts";
14
+ import { CollaborationAdmission, type CollaborationAuthority } from "./collaboration-admission.ts";
15
+ import { testCrashPoint } from "../test-crash.ts";
16
+ import type { NativeDocumentBuild } from "../attachments/document-reader.ts";
17
+ import type { ArtifactReviewSummary, ArtifactSecurityHold } from "../security-allow.ts";
18
+ import { presentArtifactRead, type ArtifactReadArguments, type PresentedRead } from "./collaboration-read.ts";
19
+
20
+ const invalid = (): never => { throw new CollaborationStoreError("invalid_payload", 400); };
21
+ function keys(value: unknown, allowed: readonly string[]): Record<string, unknown> {
22
+ if (!record(value) || Object.keys(value).some(key => !allowed.includes(key))) return invalid();
23
+ return value;
24
+ }
25
+ function string(value: unknown): string { return text(value) ? value : invalid(); }
26
+ function strings(value: unknown): readonly string[] {
27
+ if (value === undefined) return [];
28
+ if (!Array.isArray(value) || !value.every(text) || new Set(value).size !== value.length) return invalid();
29
+ if (value.length > 10) throw new CollaborationStoreError("request_too_large", 413);
30
+ return value;
31
+ }
32
+ function integer(value: unknown): number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : invalid(); }
33
+ type Payload =
34
+ | { readonly operation: "request"; readonly agentId: string; readonly kind: "task" | "question"; readonly text: string; readonly files: readonly string[]; readonly retryOf?: string }
35
+ | { readonly operation: "requests"; readonly requestId?: string }
36
+ | { readonly operation: "cancel"; readonly requestId: string; readonly expectedRevision: number; readonly reason: string }
37
+ | { readonly operation: "result"; readonly requestId: string; readonly status: "completed" | "failed"; readonly text: string; readonly outputs: readonly string[] }
38
+ | { readonly operation: "files.stage"; readonly agentId: string; readonly source: { readonly kind: "path"; readonly path: string } | { readonly kind: "attachment"; readonly id: string } | { readonly kind: "artifact"; readonly requestId: string; readonly id: string } }
39
+ | { readonly operation: "files.list"; readonly requestId: string }
40
+ | { readonly operation: "files.read"; readonly requestId: string; readonly artifactId: string; readonly view?: "text" | "pages"; readonly cursor?: string; readonly limit?: number };
41
+ function parse(operation: unknown, value: unknown): Payload {
42
+ switch (operation) {
43
+ case "request": {
44
+ const p = keys(value, ["agentId", "kind", "text", "files", "retryOf"]);
45
+ if (p.kind !== "task" && p.kind !== "question") return invalid();
46
+ return { operation, agentId: string(p.agentId), kind: p.kind, text: string(p.text), files: strings(p.files),
47
+ ...(p.retryOf === undefined ? {} : { retryOf: string(p.retryOf) }) };
48
+ }
49
+ case "requests": {
50
+ const p = keys(value, ["requestId"]);
51
+ return { operation, ...(p.requestId === undefined ? {} : { requestId: string(p.requestId) }) };
52
+ }
53
+ case "cancel": {
54
+ const p = keys(value, ["requestId", "expectedRevision", "reason"]);
55
+ return { operation, requestId: string(p.requestId), expectedRevision: integer(p.expectedRevision), reason: string(p.reason) };
56
+ }
57
+ case "result": {
58
+ const p = keys(value, ["requestId", "status", "text", "outputs"]);
59
+ if (p.status !== "completed" && p.status !== "failed") return invalid();
60
+ return { operation, requestId: string(p.requestId), status: p.status, text: string(p.text), outputs: strings(p.outputs) };
61
+ }
62
+ case "files.stage": {
63
+ const p = keys(value, ["operation", "agentId", "path", "attachmentId", "scopedArtifact"]);
64
+ if (p.operation !== "stage" || ["path", "attachmentId", "scopedArtifact"].filter(k => p[k] !== undefined).length !== 1) return invalid();
65
+ const agentId = string(p.agentId);
66
+ if (p.path !== undefined) return { operation, agentId, source: { kind: "path", path: string(p.path) } };
67
+ if (p.attachmentId !== undefined) return { operation, agentId, source: { kind: "attachment", id: string(p.attachmentId) } };
68
+ const s = keys(p.scopedArtifact, ["requestId", "artifactId"]);
69
+ return { operation, agentId, source: { kind: "artifact", requestId: string(s.requestId), id: string(s.artifactId) } };
70
+ }
71
+ case "files.list": {
72
+ const p = keys(value, ["operation", "requestId"]);
73
+ if (p.operation !== "list") return invalid();
74
+ return { operation, requestId: string(p.requestId) };
75
+ }
76
+ case "files.read": {
77
+ const p = keys(value, ["operation", "requestId", "artifactId", "view", "cursor", "limit"]);
78
+ if (p.operation !== "read" || (p.view !== undefined && p.view !== "text" && p.view !== "pages")) return invalid();
79
+ const limit = p.limit === undefined ? undefined : integer(p.limit);
80
+ if (limit === 0 || (p.view === undefined && (p.cursor !== undefined || limit !== undefined))) return invalid();
81
+ return { operation, requestId: string(p.requestId), artifactId: string(p.artifactId),
82
+ ...(p.view === undefined ? {} : { view: p.view }), ...(p.cursor === undefined ? {} : { cursor: string(p.cursor) }),
83
+ ...(limit === undefined ? {} : { limit }) };
84
+ }
85
+ default: return invalid();
86
+ }
87
+ }
88
+ const metadata = (a: AgentArtifact) => ({ artifactId: a.id, requestId: a.requestId, sha256: a.sha256, bytes: a.bytes,
89
+ name: a.originalName, mediaType: a.mediaType, storageClass: a.storageClass, state: a.state });
90
+ /** 294's owner principal (ToolPermissions owner principals are always this ref); the review route compares against it. */
91
+ const OWNER_PRINCIPAL = "owner";
92
+ const unavailable = (capability: string) => Response.json({ error: "capability_unavailable", capability }, { status: 503 });
93
+
94
+ /** HTTP boundary only. Hub owns execution; Store owns admission, lineage, grants and state transitions. */
95
+ export class AgentCollaboration {
96
+ private readonly sessions: CollaborationSessions;
97
+ private readonly admission: CollaborationAdmission;
98
+ constructor(private readonly host: { readonly hub: Hub; readonly store: Store; readonly files: AgentFileStore;
99
+ readonly authority?: CollaborationAuthority;
100
+ readonly artifactReadAdmission?: (hold: ArtifactSecurityHold) => CollaborationGate;
101
+ /** The 293 native document reader for text/pages views. Absent or null: those views answer native_reader_unavailable. */
102
+ readonly documentReader?: NativeDocumentBuild | null;
103
+ /** Owner authentication at the caller boundary (294 C3 helper). Absent: owner actions answer 503, never succeed. */
104
+ readonly owner?: (req: Request, trusted: boolean) => { readonly ownerRef: string } | Response }, private readonly token: string) {
105
+ this.sessions = new CollaborationSessions(host.hub, host.store);
106
+ this.admission = new CollaborationAdmission(host.store, host.files, host.authority);
107
+ host.hub.attachCollaboration({ bind: (conv, input, dispatch) => this.sessions.bind(conv, input, dispatch),
108
+ retire: conv => this.sessions.retire(conv), gate: request => this.admission.gate(request),
109
+ permission: outcome => this.admission.permission(outcome),
110
+ artifactRead: hold => {
111
+ if (host.artifactReadAdmission) return host.artifactReadAdmission(hold);
112
+ if (hold.representation.view !== "raw" && !host.documentReader) throw new CollaborationStoreError("document_representations_unavailable", 503);
113
+ return this.admission.gate();
114
+ } });
115
+ }
116
+ /**
117
+ * Owner routes under /api/agent-collaboration. Listing reads the same history the trusted web already shows; cancel
118
+ * and file download are owner actions and need the injected owner principal.
119
+ */
120
+ async handleOwner(req: Request, trusted: boolean): Promise<Response> {
121
+ if (!trusted) return Response.json({ error: "forbidden" }, { status: 403 });
122
+ const url = new URL(req.url), { store, files } = this.host;
123
+ try {
124
+ if (url.pathname === "/api/agent-collaboration/requests" && req.method === "GET") {
125
+ const conv = url.searchParams.get("conv");
126
+ if (!conv || !store.getConversation(conv)) return Response.json({ error: "conversation_not_found" }, { status: 404 });
127
+ const page = store.listAgentRequests(conv, url.searchParams.get("cursor") ?? undefined);
128
+ return Response.json({ requests: page.requests.map(request => this.participant(request.id, conv)), nextCursor: page.nextCursor });
129
+ }
130
+ const cancel = url.pathname.match(/^\/api\/agent-collaboration\/requests\/([^/]+)\/cancel$/);
131
+ const download = url.pathname.match(/^\/api\/agent-collaboration\/artifacts\/([^/]+)$/);
132
+ const review = url.pathname.match(/^\/api\/agent-collaboration\/holds\/([^/]+)$/);
133
+ if (!cancel && !download && !review) return Response.json({ error: "not_found" }, { status: 404 });
134
+ if (!this.host.owner) return Response.json({ error: "owner_auth_unavailable" }, { status: 503 });
135
+ const owner = this.host.owner(req, trusted);
136
+ if (owner instanceof Response) return owner;
137
+ if (cancel && req.method === "POST") {
138
+ const body: unknown = await req.json();
139
+ if (!record(body) || !Number.isSafeInteger(body.expectedRevision)) return invalid();
140
+ const receipt = store.cancelAgentRequestAsOwner({ requestId: decodeURIComponent(cancel[1]!), expectedRevision: Number(body.expectedRevision), reason: "owner_cancelled" });
141
+ if (receipt.state === "cancel_requested") void this.host.hub.interruptCancelledCollaboration().catch(error => console.error("collaboration cancel interrupt:", error));
142
+ this.host.hub.wakeCollaborationReplies();
143
+ return Response.json(receipt);
144
+ }
145
+ if (review && req.method === "POST") {
146
+ const body: unknown = await req.json();
147
+ if (!record(body) || (body.choice !== "allow" && body.choice !== "deny") || !Number.isSafeInteger(body.expectedRevision) || !text(body.submissionId)) return invalid();
148
+ return Response.json(this.reviewHold({ holdId: decodeURIComponent(review[1]!), choice: body.choice, expectedRevision: Number(body.expectedRevision),
149
+ submissionId: body.submissionId, ownerPrincipalRef: owner.ownerRef }));
150
+ }
151
+ if (download && (req.method === "GET" || req.method === "POST")) {
152
+ const artifact = store.agentArtifact(decodeURIComponent(download[1]!));
153
+ // A trashed artifact keeps its tombstone row; its bytes are gone for good, which is 410, not an unknown ID.
154
+ if (artifact?.state === "trashed") return Response.json({ error: "artifact_revoked" }, { status: 410 });
155
+ if (!artifact || artifact.state !== "committed") return Response.json({ error: "artifact_not_found" }, { status: 404 });
156
+ using core = new AgentContentFiles(files.root);
157
+ const bytes = core.read([".collaboration", "artifacts", artifact.sealedName]);
158
+ if (createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) return Response.json({ error: "file_changed" }, { status: 409 });
159
+ return new Response(new Uint8Array(bytes), { headers: { "content-type": artifact.mediaType || "application/octet-stream",
160
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(artifact.originalName)}`, "x-zgw-artifact-sha256": artifact.sha256 } });
161
+ }
162
+ return Response.json({ error: "method_not_allowed" }, { status: 405 });
163
+ } catch (error) {
164
+ if (error instanceof CollaborationStoreError || error instanceof AgentContentError) return Response.json({ error: error.code }, { status: error.status });
165
+ if (error instanceof SyntaxError) return invalid();
166
+ throw error;
167
+ }
168
+ }
169
+ /** Housekeeping candidates and the operator-selected move to the Trash (the Store re-checks every reference). */
170
+ cleanupRows() {
171
+ return this.host.store.agentArtifactCleanupRows().map(row => ({ artifactId: row.artifactId, state: row.state, bytes: row.bytes,
172
+ target: join(this.host.files.root, ".collaboration", "artifacts", row.sealedName) }));
173
+ }
174
+ trashArtifact(artifactId: string): void {
175
+ using core = new AgentContentFiles(this.host.files.root);
176
+ new AgentArtifacts(core, this.host.store, this.admission.gate()).trash(artifactId);
177
+ }
178
+ async handle(req: Request, trusted: boolean): Promise<Response> {
179
+ const denied = maintenanceAuth(req, trusted, this.token);
180
+ if (denied) return denied;
181
+ return this.host.hub.admitWorkRequest(() => this.dispatch(req));
182
+ }
183
+ private async dispatch(req: Request): Promise<Response> {
184
+ try {
185
+ const url = new URL(req.url), download = url.pathname.match(/^\/internal\/agent-collaboration\/artifacts\/([^/]+)$/);
186
+ const upload = url.pathname === "/internal/agent-collaboration/files";
187
+ if (download ? req.method !== "GET" : req.method !== "POST") return invalid();
188
+ if (!download && !upload && url.pathname !== "/internal/agent-collaboration") return Response.json({ error: "not_found" }, { status: 404 });
189
+ if (!download && url.search) return invalid();
190
+ const chunks: Uint8Array[] = [];
191
+ let length = 0;
192
+ if (req.body) for await (const chunk of req.body) {
193
+ length += chunk.length;
194
+ if (length > (upload ? 11 * 1024 * 1024 : 512 * 1024)) throw new CollaborationStoreError("body_too_large", 413);
195
+ chunks.push(chunk);
196
+ }
197
+ const body = Buffer.concat(chunks);
198
+ let envelope: Record<string, unknown>, file: File | undefined;
199
+ if (download) {
200
+ if ([...url.searchParams.keys()].some(k => k !== "requestId") || url.searchParams.getAll("requestId").length !== 1) return invalid();
201
+ envelope = { operation: "files.read", payload: { operation: "read", requestId: url.searchParams.get("requestId"), artifactId: download[1] } };
202
+ } else if (upload) {
203
+ const form = await new Request(req.url, { method: "POST", headers: req.headers, body }).formData();
204
+ if ([...form.keys()].length !== 2 || !(form.get("file") instanceof File) || typeof form.get("metadata") !== "string") return invalid();
205
+ const part = form.get("file"), description = form.get("metadata");
206
+ if (!(part instanceof File) || typeof description !== "string") return invalid();
207
+ file = part;
208
+ envelope = keys(JSON.parse(description), ["operation", "payload", "sourceSessionPath", "toolCallId", "context"]);
209
+ } else envelope = keys(await new Response(body).json(), ["operation", "payload", "sourceSessionPath", "toolCallId", "context"]);
210
+ const payload = parse(envelope.operation, envelope.payload);
211
+ if ("text" in payload && Buffer.byteLength(payload.text) > 65536) throw new CollaborationStoreError("request_too_large", 413);
212
+ if (upload && payload.operation !== "files.stage") return invalid();
213
+ const proof: unknown = JSON.parse(req.headers.get("x-zgw-managed-proof") ?? "null");
214
+ const uploaded = file ? Buffer.from(await file.arrayBuffer()) : undefined;
215
+ const logicalPayload = { ...(payload.operation === "request" ? { ...payload, text: payload.text.trim(), files: [...payload.files].sort() } : payload),
216
+ ...(uploaded ? { fileSha256: createHash("sha256").update(uploaded).digest("hex") } : {}) };
217
+ const call = await this.sessions.authenticate({ method: req.method, route: url.pathname + url.search, operation: payload.operation, body, proof, logicalPayload });
218
+ if (!download && (envelope.sourceSessionPath !== call.binding.sessionPath || envelope.toolCallId !== call.verified.toolCallId ||
219
+ !isDeepStrictEqual(envelope.context, { requestId: call.requestId, rootId: call.rootId, binding: call.binding }))) return invalid();
220
+ call.assertCurrent();
221
+ const { store, files, hub } = this.host;
222
+ if (call.binding.activationId !== null) {
223
+ await hub.recordCollaborationConsumption(call.binding.convId, call.binding.inputId);
224
+ call.assertCurrent();
225
+ }
226
+ const policy = this.admission.gate();
227
+ const gate: CollaborationGate = { ...policy, assertAccess: (tx, source, target) => {
228
+ call.assertCurrent();
229
+ policy.assertAccess(tx, source, target);
230
+ return undefined;
231
+ } };
232
+ switch (payload.operation) {
233
+ case "request": {
234
+ hub.checkCollaborationText(payload.text);
235
+ const cursors = new Map(store.listConversations().map(c => [c.id, store.recentEvents(c.id, 1).at(-1)?.seq ?? 0]));
236
+ const before = new Set(store.eventsAfter(call.binding.convId, 0).flatMap(event =>
237
+ event.type === "message.collaboration" && record(event.data) && text(event.data.requestId) ? [event.data.requestId] : []));
238
+ const receipt = store.acceptAgentRequest({ toolCallId: call.verified.toolCallId, targetAgentId: payload.agentId,
239
+ kind: payload.kind, text: payload.text, files: payload.files, ...(payload.retryOf ? { retryOf: payload.retryOf } : {}) }, call.binding, gate);
240
+ testCrashPoint("collaboration.admission.after-commit");
241
+ hub.collaborationQueued(receipt.requestId, cursors);
242
+ return Response.json(receipt, { status: before.has(receipt.requestId) ? 200 : 202 });
243
+ }
244
+ case "requests": {
245
+ if (payload.requestId) return Response.json(this.participant(payload.requestId, call.binding.convId));
246
+ const page = store.listAgentRequests(call.binding.convId);
247
+ return Response.json({ requests: page.requests.map(r => this.participant(r.id, call.binding.convId)), nextCursor: page.nextCursor });
248
+ }
249
+ case "cancel": {
250
+ const receipt = store.cancelAgentRequest(payload, call.binding);
251
+ if (receipt.state === "cancel_requested") void hub.interruptCancelledCollaboration().catch(error => console.error("collaboration cancel interrupt:", error));
252
+ hub.wakeCollaborationReplies();
253
+ return Response.json(receipt);
254
+ }
255
+ case "result": {
256
+ const request = store.getAgentRequest(payload.requestId);
257
+ if (!request) throw new CollaborationStoreError("request_not_found", 404);
258
+ if (request.targetConv !== call.binding.convId || call.binding.activationId === null) throw new CollaborationStoreError("result_forbidden", 403);
259
+ const submitted = { requestId: request.id, toolCallId: call.verified.toolCallId, outcome: payload.status, text: payload.text };
260
+ const prior = store.agentResultIntent(call.binding.activationId);
261
+ if (prior) {
262
+ // A wire retry of the same submission returns its receipt; it never seals the outputs again.
263
+ return Response.json(store.recordAgentResultIntent({ ...prior, ...submitted, expectedRevision: prior.expectedRevision }, call.binding), { status: 200 });
264
+ }
265
+ // The extension froze further writes at this result execute; it drains and seals the exact listed outputs.
266
+ const sealed = await hub.collaborationSnapshot(call.binding.convId, { requestId: request.id, requestRevision: request.revision,
267
+ operationKey: call.operationKey, bindingDigest: collaborationDigest(call.binding), paths: payload.outputs });
268
+ call.assertCurrent();
269
+ if (!sealed.ok) throw new CollaborationStoreError(sealed.error, sealed.error === "file_not_found" ? 404 : sealed.error === "file_too_large" ? 413 : 409);
270
+ const outputs = (() => {
271
+ using core = new AgentContentFiles(files.root);
272
+ return sealed.files.map(file => {
273
+ const bytes = core.read([".collaboration", "artifacts", file.sealedName]);
274
+ if (bytes.length !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.sha256) throw new CollaborationStoreError("file_changed");
275
+ return { sha256: file.sha256, bytes: file.bytes, mediaType: file.mediaType, name: file.name, sealedName: file.sealedName };
276
+ });
277
+ })();
278
+ const artifacts = store.prepareAgentResult({ requestId: request.id, expectedRevision: request.revision, files: outputs }, call.binding);
279
+ const prepared = store.getAgentRequest(request.id);
280
+ if (!prepared) throw new CollaborationStoreError("request_not_found", 404);
281
+ const receipt = store.recordAgentResultIntent({ ...submitted, expectedRevision: prepared.revision, artifacts: artifacts.map(a => a.id) }, call.binding);
282
+ return Response.json({ ...receipt, files: artifacts.map(metadata) }, { status: 202 });
283
+ }
284
+ case "files.stage": {
285
+ switch (payload.source.kind) {
286
+ case "attachment": return unavailable("attachment_export_metadata");
287
+ case "artifact": return unavailable("collaboration_transfer_authority");
288
+ case "path": break;
289
+ default: return collaborationNever(payload.source);
290
+ }
291
+ if (!file || !uploaded) return invalid();
292
+ if (file.size > 10 * 1024 * 1024) throw new CollaborationStoreError("file_too_large", 413);
293
+ const bytes = uploaded;
294
+ call.assertCurrent();
295
+ const target = store.listConversations(false).find(c => c.agentId === payload.agentId);
296
+ if (!target) throw new CollaborationStoreError("agent_not_found", 404);
297
+ const stage = store.beginAgentStage({ operationKey: call.operationKey,
298
+ source: { kind: "agent_path", path: payload.source.path, sha256: createHash("sha256").update(bytes).digest("hex") },
299
+ target: { agentId: payload.agentId, convId: target.id, lifecycleEpoch: store.ensureAgentLifecycle(payload.agentId) },
300
+ ownerPrincipalRef: "", authority: null }, call.binding, gate);
301
+ using core = new AgentContentFiles(files.root);
302
+ const artifact = await new AgentArtifacts(core, store, gate).stage(stage.transferId, async () => ({ bytes, name: file.name, mediaType: file.type || "application/octet-stream" }));
303
+ call.assertCurrent();
304
+ return Response.json({ ...metadata(artifact), transferId: stage.transferId }, { status: stage.state === "staged" ? 200 : 202 });
305
+ }
306
+ case "files.list": {
307
+ const request = store.getAgentRequest(payload.requestId);
308
+ this.participant(payload.requestId, call.binding.convId);
309
+ return Response.json({ files: (request?.payload.files ?? []).flatMap(id => {
310
+ const a = store.agentArtifact(id), grant = store.agentArtifactGrant(id, call.binding.convId, payload.requestId);
311
+ return a && grant && !grant.revoked ? [metadata(a)] : [];
312
+ }) });
313
+ }
314
+ case "files.read": {
315
+ this.participant(payload.requestId, call.binding.convId);
316
+ const { operation: _operation, ...args } = payload;
317
+ using core = new AgentContentFiles(files.root);
318
+ call.assertCurrent();
319
+ const artifacts = new AgentArtifacts(core, store, gate);
320
+ const present = async (artifact: AgentArtifact, bytes: Buffer) => {
321
+ const presented = await presentArtifactRead({ artifact, bytes, args, reader: this.host.documentReader });
322
+ call.assertCurrent();
323
+ return { presented, bytes };
324
+ };
325
+ type Shown = { readonly presented: PresentedRead; readonly bytes: Buffer } | { readonly held: Record<string, unknown> };
326
+ const continuation = call.binding.readContinuationId;
327
+ const shown: Shown = continuation === undefined
328
+ ? await artifacts.readReviewed<Shown>({ ...args, readOperationId: call.operationId }, call.binding, async (artifact, bytes, readId) => {
329
+ const value = await present(artifact, bytes);
330
+ const summary = this.review(value.presented);
331
+ if (summary === null) return { release: true, value };
332
+ return { release: false, value: { held: this.holdRead(artifact, readId, value.presented, args, call.binding, gate, summary) } };
333
+ })
334
+ : await artifacts.releaseHeld<Shown>(this.continuationHold(continuation, args), call.binding, async (artifact, bytes) => {
335
+ const value = await present(artifact, bytes);
336
+ return { sha256: value.presented.representation.sha256, value };
337
+ });
338
+ call.assertCurrent();
339
+ if ("held" in shown) return Response.json(shown.held, { status: 202 });
340
+ if (download) return new Response(new Uint8Array(shown.bytes), { headers: { "content-type": "application/octet-stream",
341
+ "x-zgw-artifact-id": payload.artifactId, "x-zgw-request-id": payload.requestId } });
342
+ return Response.json(shown.presented.body);
343
+ }
344
+ default: return collaborationNever(payload);
345
+ }
346
+ } catch (error) {
347
+ if (error instanceof ManagedCallError) return Response.json({ error: error.code }, { status: 403 });
348
+ if (error instanceof OmoCommandError) return Response.json({ error: "collaboration_native_unavailable" }, { status: 503 });
349
+ if (error instanceof CollaborationStoreError || error instanceof AgentContentError) return Response.json({ error: error.code }, { status: error.status });
350
+ if (error instanceof SyntaxError || error instanceof TypeError) return Response.json({ error: "invalid_payload" }, { status: 400 });
351
+ throw error;
352
+ }
353
+ }
354
+ /**
355
+ * The gateway's message security check (MessageGuard through the Hub) over every text a read would release, plus the
356
+ * pages no text layer covered: null when the read may go out as is, else what the owner's review card shows.
357
+ */
358
+ private review(presented: PresentedRead): ArtifactReviewSummary | null {
359
+ const findings = this.host.hub.collaborationFindings(presented.texts).map(({ kind, count }) => ({ kind, count }));
360
+ if (findings.length === 0 && presented.unscannedPages.length === 0) return null;
361
+ return { findings, unscannedPages: [...presented.unscannedPages] };
362
+ }
363
+ /**
364
+ * Flagged content stays unreleased. A peer activation reading a sealed result gets the owner review hold and its inbox
365
+ * decision; any other flagged read (owner turn, request input) has no reviewable continuation and is refused.
366
+ */
367
+ private holdRead(artifact: AgentArtifact, readId: string, presented: PresentedRead, args: ArtifactReadArguments, binding: import("./collaboration-types.ts").CollaborationBinding, gate: CollaborationGate, summary: ArtifactReviewSummary) {
368
+ const { store, hub } = this.host;
369
+ const activation = binding.activationId === null ? null : store.getAgentActivation(binding.activationId);
370
+ if (!activation || !artifact.resultId) throw new CollaborationStoreError("collaboration_security_review_required");
371
+ const hold = store.createArtifactSecurityHold({ readOperationId: readId, originInputSeq: activation.inputSeq, ownerPrincipalRef: OWNER_PRINCIPAL,
372
+ representation: { ...presented.representation, revision: 1, scanRevision: `message-guard:${store.securityAllowedValues().revision}` }, readArguments: args, summary }, binding, gate);
373
+ hub.refreshDecisions();
374
+ return { state: "held", holdId: hold.id, decisionId: hold.decisionId, revision: hold.revision, requestId: args.requestId, artifactId: args.artifactId,
375
+ view: args.view ?? "raw", contentReleased: false, unscannedPages: hold.summary?.unscannedPages ?? [] };
376
+ }
377
+ private continuationHold(continuationId: string, args: ArtifactReadArguments): ArtifactSecurityHold {
378
+ const hold = this.host.store.artifactHoldByContinuation(continuationId);
379
+ if (!hold || !isDeepStrictEqual(hold.readArguments, args)) throw new CollaborationStoreError("artifact_review_stale");
380
+ return hold;
381
+ }
382
+ private reviewHold(input: { readonly holdId: string; readonly choice: "allow" | "deny"; readonly expectedRevision: number; readonly submissionId: string; readonly ownerPrincipalRef: string }) {
383
+ const { store, hub } = this.host;
384
+ const hold = store.artifactSecurityHold(input.holdId);
385
+ if (!hold) throw new CollaborationStoreError("hold_not_found", 404);
386
+ const receipt = store.reviewArtifactHold({ ...input, scopeDigest: hold.scopeDigest }, store.artifactReaderBinding(hold.readOperationId), this.admission.gate());
387
+ if (receipt.reviewState === "allowed") hub.resumeArtifactReview(hold.id);
388
+ else hub.refreshDecisions();
389
+ return { ...receipt, resumeState: store.artifactSecurityHold(hold.id)?.continuation.state ?? null };
390
+ }
391
+ private participant(id: string, conv: string) {
392
+ const request = this.host.store.getAgentRequest(id);
393
+ if (!request) throw new CollaborationStoreError("request_not_found", 404);
394
+ if (request.requesterConv !== conv && request.targetConv !== conv) throw new CollaborationStoreError("request_forbidden", 403);
395
+ // F3 D5: the recipient agent names a sent card's receiver; the stored request message carries only its sender.
396
+ return { requestId: request.id, rootId: request.rootId, parentId: request.parentId, kind: request.kind, targetAgent: request.targetAgent,
397
+ state: request.state, revision: request.revision, resultId: request.resultId, text: request.payload.text, files: request.payload.files };
398
+ }
399
+ }