zigmini 1.0.0-beta.2 → 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.
- package/README.ko.md +36 -3
- package/README.md +36 -3
- package/assets/agent/gateway-request-rules.md +1 -1
- package/assets/agent/gateway-rules.md +6 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/assets/document-reader/document-reader +0 -0
- package/assets/document-reader/host-selector +0 -0
- package/assets/document-reader/manifest.json +27 -0
- package/docs/manual.ko.md +120 -11
- package/docs/manual.md +123 -12
- package/host/document-reader/DocumentRead.swift +370 -0
- package/host/document-reader/HostSelector.swift +192 -0
- package/host/document-reader/Protocol.swift +647 -0
- package/host/document-reader/main.swift +22 -0
- package/host/omo-bridge/agent-aside-tool.ts +119 -0
- package/host/omo-bridge/agent-aside.ts +522 -0
- package/host/omo-bridge/agent-collaboration-files.ts +282 -0
- package/host/omo-bridge/agent-collaboration.ts +385 -0
- package/host/omo-bridge/agent-control.ts +400 -28
- package/host/omo-bridge/agent-execution.sb +42 -0
- package/host/omo-bridge/agent-execution.ts +158 -0
- package/host/omo-bridge/agent-tool-adapters.ts +388 -0
- package/host/omo-bridge/agent-tool-permissions.ts +96 -0
- package/host/omo-bridge/aside-tabs.ts +41 -0
- package/host/omo-bridge/attachment-read.ts +100 -0
- package/host/omo-bridge/bridge.ts +10 -0
- package/host/omo-bridge/managed-call.ts +46 -0
- package/host/omo-bridge/task-control.ts +39 -5
- package/host/omo-check/patches/attachments-293/README.md +138 -0
- package/host/omo-check/patches/attachments-293/document-content.js +234 -0
- package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
- package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
- package/host/omo-check/patches/omo-5.1.51-chatgpt-terra.patch +21 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +44 -6
- package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
- package/package.json +5 -3
- package/src/agent/api.ts +345 -20
- package/src/agent/artifacts.ts +196 -0
- package/src/agent/aside-target-routes.ts +369 -0
- package/src/agent/aside-targets.ts +215 -0
- package/src/agent/collaboration-admission.ts +47 -0
- package/src/agent/collaboration-read.ts +117 -0
- package/src/agent/collaboration-session.ts +196 -0
- package/src/agent/collaboration-types.ts +258 -0
- package/src/agent/collaboration.ts +399 -0
- package/src/agent/definition.ts +65 -3
- package/src/agent/fields.ts +9 -3
- package/src/agent/file-core.ts +325 -0
- package/src/agent/files.ts +87 -8
- package/src/agent/list.ts +3 -3
- package/src/agent/managed-call.ts +144 -0
- package/src/agent/tool-catalog.ts +184 -0
- package/src/agent/tool-delivery.ts +359 -0
- package/src/agent/tool-permission-types.ts +81 -0
- package/src/agent/tool-permissions.ts +1366 -0
- package/src/attachments/api.ts +161 -0
- package/src/attachments/document-reader.ts +325 -0
- package/src/attachments/host-selector.ts +410 -0
- package/src/attachments/managed-binding.ts +83 -0
- package/src/attachments/prepare.ts +214 -0
- package/src/attachments/selector-port.ts +28 -0
- package/src/attachments/service.ts +729 -0
- package/src/attachments/store.ts +698 -0
- package/src/attachments/types.ts +123 -0
- package/src/cli/doctor.ts +30 -10
- package/src/decisions.ts +109 -10
- package/src/engine/apply-patch.ts +21 -0
- package/src/engine/compat.json +2 -1
- package/src/engine/install.ts +45 -4
- package/src/engine/sandbox-cell.ts +2 -0
- package/src/gc.ts +33 -225
- package/src/housekeeping/api.ts +163 -0
- package/src/housekeeping/audit.ts +61 -0
- package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
- package/src/housekeeping/collectors/agent-drafts.ts +43 -0
- package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
- package/src/housekeeping/collectors/agent-trash.ts +28 -0
- package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
- package/src/housekeeping/collectors/db-backup.ts +25 -0
- package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
- package/src/housekeeping/collectors/docker-image.ts +135 -0
- package/src/housekeeping/collectors/omo-version.ts +32 -0
- package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
- package/src/housekeeping/collectors/reconcile-work.ts +55 -0
- package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
- package/src/housekeeping/collectors/shared.ts +149 -0
- package/src/housekeeping/collectors/temp-dir.ts +28 -0
- package/src/housekeeping/collectors/update-file.ts +25 -0
- package/src/housekeeping/collectors/worktree.ts +193 -0
- package/src/housekeeping/engine.ts +125 -0
- package/src/housekeeping/reconcile/work.ts +130 -0
- package/src/housekeeping/registry.ts +63 -0
- package/src/housekeeping/trash.ts +59 -0
- package/src/housekeeping/types.ts +31 -0
- package/src/hub.ts +1095 -41
- package/src/main.ts +230 -31
- package/src/message-references.ts +14 -5
- package/src/profile-prompt.ts +13 -1
- package/src/profile.ts +13 -5
- package/src/protocol.ts +41 -6
- package/src/security-allow.ts +79 -2
- package/src/security-check.ts +1 -1
- package/src/store.ts +1946 -23
- package/src/test-crash.ts +20 -0
- package/src/usage.ts +18 -7
- package/src/work-watch.ts +55 -7
- package/src/work.ts +192 -3
- package/web/DESIGN.md +35 -1
- package/web/agents.d.ts +189 -12
- package/web/agents.js +898 -39
- package/web/app.js +2634 -210
- package/web/attachments.d.ts +91 -0
- package/web/attachments.js +422 -0
- package/web/collaboration.d.ts +87 -0
- package/web/collaboration.js +290 -0
- package/web/decisions.d.ts +39 -2
- package/web/decisions.js +354 -2
- package/web/gc.d.ts +26 -5
- package/web/gc.js +113 -13
- package/web/i18n/agents.d.ts +4 -0
- package/web/i18n/agents.js +200 -8
- package/web/i18n/attachments.js +78 -0
- package/web/i18n/decisions.d.ts +4 -0
- package/web/i18n/decisions.js +68 -0
- package/web/i18n/gc.d.ts +4 -0
- package/web/i18n/gc.js +184 -2
- package/web/i18n/main.js +80 -8
- package/web/i18n/tour.d.ts +4 -0
- package/web/i18n/tour.js +37 -0
- package/web/index.html +20 -3
- package/web/profile.d.ts +2 -2
- package/web/profile.js +16 -10
- package/web/style.css +342 -21
- package/web/tooltip.d.ts +2 -0
- package/web/tooltip.js +12 -0
- package/web/tour.d.ts +57 -0
- package/web/tour.js +450 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { createHash, createSecretKey, randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { collaborationDigest, type CollaborationBinding } from "../../src/agent/collaboration-types.ts";
|
|
5
|
+
import { parseManagedCallIdentity, type ManagedCallIdentity, type ManagedCallProof, type ManagedCallRequest } from "../../src/agent/managed-call.ts";
|
|
6
|
+
import { runtimePaths } from "../../src/runtime/paths.ts";
|
|
7
|
+
import { buildManagedCallProof, type ManagedNativeContext } from "./managed-call.ts";
|
|
8
|
+
export { CollaborationSnapshots } from "./agent-collaboration-files.ts";
|
|
9
|
+
|
|
10
|
+
export type SnapshotCall = { readonly requestId: string; readonly requestRevision: number; readonly operationKey: string; readonly bindingDigest: string; readonly paths: readonly string[] };
|
|
11
|
+
export type SnapshotTicket = { readonly operationKey: string; readonly requestId: string; readonly requestRevision: number; readonly binding: CollaborationBinding };
|
|
12
|
+
export type SealedOutput = { readonly path: string; readonly sealedName: string; readonly sha256: string; readonly bytes: number; readonly mediaType: string; readonly name: string };
|
|
13
|
+
export type SnapshotError = "invalid_snapshot" | "snapshot_conflict" | "binding_conflict" | "request_blocked" | "read_only_continuation" |
|
|
14
|
+
"agent_knowledge_locked" | "snapshot_not_quiescent" | "file_not_found" | "file_changed" | "file_not_regular" | "file_linked" |
|
|
15
|
+
"path_forbidden" | "file_too_large" | "snapshot_io_failed" | "snapshot_unknown";
|
|
16
|
+
export type SnapshotResult = { readonly ok: true; readonly snapshotKey: string; readonly requestId: string; readonly bindingDigest: string; readonly files: readonly SealedOutput[] } |
|
|
17
|
+
{ readonly ok: false; readonly error: SnapshotError };
|
|
18
|
+
export interface SnapshotPort { snapshot(requestId: string, paths: readonly string[]): Promise<SnapshotResult> }
|
|
19
|
+
export interface CollaborationInvocation extends ManagedCallRequest { readonly proof: ManagedCallProof; readonly signal?: AbortSignal; readonly contentType?: string }
|
|
20
|
+
export type StageFile = { readonly bytes: Uint8Array; readonly path: string; readonly name: string; readonly mediaType: string };
|
|
21
|
+
export interface CollaborationTool {
|
|
22
|
+
readonly name: string; readonly label: string; readonly description: string; readonly parameters: unknown;
|
|
23
|
+
readonly serverPinned?: boolean;
|
|
24
|
+
execute(id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, update: unknown, ctx: unknown): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface CollaborationApi {
|
|
27
|
+
readonly sessionKind: string; readonly sessionContext: Readonly<Record<string, string>>;
|
|
28
|
+
readonly rpc: { handle(name: string, handler: (data: unknown) => unknown): void };
|
|
29
|
+
registerTool(tool: CollaborationTool): void;
|
|
30
|
+
}
|
|
31
|
+
type Native = ManagedNativeContext & { readonly sessionManager: ManagedNativeContext["sessionManager"] & { getEntries(): readonly unknown[] } };
|
|
32
|
+
type Bound = {
|
|
33
|
+
readonly identity: ManagedCallIdentity; readonly binding: CollaborationBinding;
|
|
34
|
+
readonly requestId: string | null; readonly rootId: string | null; readonly secret: ReturnType<typeof createSecretKey>;
|
|
35
|
+
readonly read: Readonly<Record<string, unknown>> | null;
|
|
36
|
+
readonly inputEvidence: "native_event" | "client_entry";
|
|
37
|
+
readonly reservationId?: string;
|
|
38
|
+
};
|
|
39
|
+
export class CollaborationExtensionError extends Error {
|
|
40
|
+
constructor(readonly code: string) { super(code); this.name = "CollaborationExtensionError"; }
|
|
41
|
+
}
|
|
42
|
+
export const record = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
43
|
+
const text = (v: unknown): v is string => typeof v === "string" && v.length > 0;
|
|
44
|
+
const integer = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
45
|
+
const fail = (code: string): never => { throw new CollaborationExtensionError(code); };
|
|
46
|
+
/** The SDK is a runtime boundary; retain its guarded getters and original manager, never a cloned context. */
|
|
47
|
+
function isNative(value: unknown): value is Native {
|
|
48
|
+
if (!record(value) || !record(value.sessionManager)) return false;
|
|
49
|
+
const manager = value.sessionManager;
|
|
50
|
+
return typeof manager.getSessionId === "function" && typeof manager.getSessionFile === "function" && typeof manager.getEntries === "function";
|
|
51
|
+
}
|
|
52
|
+
function native(value: unknown): Native { return isNative(value) ? value : fail("native_context_mismatch"); }
|
|
53
|
+
const string = { type: "string", minLength: 1 };
|
|
54
|
+
const strings = { type: "array", items: string, maxItems: 10, uniqueItems: true };
|
|
55
|
+
const specs = {
|
|
56
|
+
request: { fields: { agentId: string, kind: { enum: ["task", "question"] }, text: string, files: strings, retryOf: string }, required: ["agentId", "kind", "text"] },
|
|
57
|
+
result: { fields: { requestId: string, status: { enum: ["completed", "failed"] }, text: string, outputs: strings }, required: ["requestId", "status", "text"] },
|
|
58
|
+
requests: { fields: { requestId: string }, required: [] },
|
|
59
|
+
cancel: { fields: { requestId: string, expectedRevision: { type: "integer", minimum: 0 }, reason: string }, required: ["requestId", "expectedRevision", "reason"] },
|
|
60
|
+
files: { fields: { operation: { enum: ["stage", "list", "read"] }, agentId: string, path: string, attachmentId: string,
|
|
61
|
+
scopedArtifact: { type: "object", properties: { requestId: string, artifactId: string }, required: ["requestId", "artifactId"], additionalProperties: false },
|
|
62
|
+
requestId: string, artifactId: string, view: { enum: ["text", "pages"] }, cursor: string, limit: { type: "integer", minimum: 1 } }, required: ["operation"] },
|
|
63
|
+
} as const;
|
|
64
|
+
type Operation = keyof typeof specs;
|
|
65
|
+
const OPERATIONS = ["request", "result", "requests", "cancel", "files"] as const;
|
|
66
|
+
export const COLLABORATION_TOOLS = OPERATIONS.map(name => `zgw_agent_${name}`);
|
|
67
|
+
function payload(operation: Operation, p: Record<string, unknown>): void {
|
|
68
|
+
const spec = specs[operation];
|
|
69
|
+
if (Object.keys(p).some(k => !Object.hasOwn(spec.fields, k)) || spec.required.some(k => !Object.hasOwn(p, k))) fail("invalid_payload");
|
|
70
|
+
for (const [key, value] of Object.entries(p)) {
|
|
71
|
+
if (["files", "outputs"].includes(key)) {
|
|
72
|
+
if (!Array.isArray(value) || value.length > 10 || !value.every(text) || new Set(value).size !== value.length) fail("invalid_payload");
|
|
73
|
+
} else if (["expectedRevision", "limit"].includes(key)) {
|
|
74
|
+
if (!integer(value) || (key === "limit" && value === 0)) fail("invalid_payload");
|
|
75
|
+
} else if (key === "scopedArtifact") {
|
|
76
|
+
if (!record(value) || Object.keys(value).length !== 2 || !text(value.requestId) || !text(value.artifactId)) fail("invalid_payload");
|
|
77
|
+
} else if (!text(value)) fail("invalid_payload");
|
|
78
|
+
}
|
|
79
|
+
if (typeof p.text === "string" && Buffer.byteLength(p.text) > 65536) fail("payload_too_large");
|
|
80
|
+
if (operation === "request" && p.kind !== "task" && p.kind !== "question") fail("invalid_payload");
|
|
81
|
+
if (operation === "result" && p.status !== "completed" && p.status !== "failed") fail("invalid_payload");
|
|
82
|
+
if (operation !== "files") return;
|
|
83
|
+
const fields = p.operation === "stage" ? ["operation", "agentId", "path", "attachmentId", "scopedArtifact"] :
|
|
84
|
+
p.operation === "list" ? ["operation", "requestId"] : p.operation === "read" ? ["operation", "requestId", "artifactId", "view", "cursor", "limit"] : [];
|
|
85
|
+
if (Object.keys(p).some(k => !fields.includes(k))) fail("invalid_payload");
|
|
86
|
+
switch (p.operation) {
|
|
87
|
+
case "stage": if (!text(p.agentId) || ["path", "attachmentId", "scopedArtifact"].filter(k => Object.hasOwn(p, k)).length !== 1) fail("invalid_payload"); break;
|
|
88
|
+
case "list": if (!text(p.requestId)) fail("invalid_payload"); break;
|
|
89
|
+
case "read": if (!text(p.requestId) || !text(p.artifactId) || (p.view !== undefined && p.view !== "text" && p.view !== "pages")) fail("invalid_payload"); break;
|
|
90
|
+
default: fail("invalid_payload");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function transport(call: CollaborationInvocation): Promise<unknown> {
|
|
94
|
+
const token = readFileSync(runtimePaths().omoTokenFile, "utf8").trim();
|
|
95
|
+
const base = process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
96
|
+
const response = await fetch(new URL(call.route, base), { method: call.method, redirect: "error",
|
|
97
|
+
headers: { authorization: `Bearer ${token}`, "content-type": call.contentType ?? "application/json", "x-zgw-managed-proof": JSON.stringify(call.proof) },
|
|
98
|
+
...(call.method === "GET" ? {} : { body: Buffer.from(call.body) }),
|
|
99
|
+
signal: call.signal ? AbortSignal.any([call.signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000) });
|
|
100
|
+
// Only 200 carries artifact bytes; a held read answers 202 with its JSON receipt.
|
|
101
|
+
if (response.status === 200 && call.method === "GET") {
|
|
102
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
103
|
+
return { artifactId: response.headers.get("x-zgw-artifact-id"), requestId: response.headers.get("x-zgw-request-id"),
|
|
104
|
+
bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
105
|
+
preview: bytes.subarray(0, 32768).toString("utf8"), truncated: bytes.length > 32768 };
|
|
106
|
+
}
|
|
107
|
+
const result: unknown = await response.json();
|
|
108
|
+
if (!response.ok) fail(`collaboration_http_${response.status}`);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/** One controller per actual extension API. Public metadata selects registration, never authority. */
|
|
112
|
+
const registrations = new WeakMap<object, CollaborationControl>();
|
|
113
|
+
export function registerAgentCollaboration(api: CollaborationApi, send: (call: CollaborationInvocation) => Promise<unknown> = transport): CollaborationControl | undefined {
|
|
114
|
+
const role = api.sessionContext.zgw_role;
|
|
115
|
+
if (api.sessionKind !== "interactive" || (role !== "main" && role !== "agent") || (role === "agent" && !api.sessionContext.zgw_agent)) return;
|
|
116
|
+
const existing = registrations.get(api);
|
|
117
|
+
if (existing) return existing;
|
|
118
|
+
const control = new CollaborationControl(role, send);
|
|
119
|
+
registrations.set(api, control);
|
|
120
|
+
api.rpc.handle("zgw.agent_collaboration_identity", () => control.identity());
|
|
121
|
+
api.rpc.handle("zgw.agent_collaboration_runtime", () => control.runtimeInfo());
|
|
122
|
+
api.rpc.handle("zgw.agent_collaboration_bind", data => control.bind(data));
|
|
123
|
+
api.rpc.handle("zgw.agent_collaboration_reservation", data => control.reservation(data));
|
|
124
|
+
api.rpc.handle("zgw.agent_collaboration_revoke", () => { control.revoke(); return { revoked: true }; });
|
|
125
|
+
api.rpc.handle("zgw.agent_snapshot", data => control.takeSnapshot(data));
|
|
126
|
+
for (const op of OPERATIONS) {
|
|
127
|
+
const spec = specs[op];
|
|
128
|
+
api.registerTool({ name: `zgw_agent_${op}`, label: `Agent ${op}`, description: `Submit or inspect an explicitly bound collaboration ${op}.`,
|
|
129
|
+
parameters: { type: "object", properties: spec.fields, required: spec.required, additionalProperties: false },
|
|
130
|
+
execute: (id, params, signal, _update, ctx) => control.execute({ id, params, signal, ctx, operation: op }) });
|
|
131
|
+
}
|
|
132
|
+
return control;
|
|
133
|
+
}
|
|
134
|
+
// allow: SIZE_OK - T5 may only extend HTTP/path-stage consumption here; moving the T4 capability lifecycle is outside its ownership.
|
|
135
|
+
/** State changes retire the exact captured capability, including calls already waiting in a file queue. */
|
|
136
|
+
export class CollaborationControl {
|
|
137
|
+
private stageFile: ((path: string, ctx: unknown, signal: AbortSignal | undefined) => Promise<StageFile>) | undefined;
|
|
138
|
+
installStage(handler: (path: string, ctx: unknown, signal: AbortSignal | undefined) => Promise<StageFile>): void { this.stageFile = handler; }
|
|
139
|
+
private runtime: Native | undefined;
|
|
140
|
+
private bound: Bound | undefined;
|
|
141
|
+
private consumed = false;
|
|
142
|
+
private consumedEntry: string | undefined;
|
|
143
|
+
private readonly retiredClientInputs = new Set<string>();
|
|
144
|
+
private readonly reservations = new Map<string, { readonly identity: ManagedCallIdentity; readonly bound: Bound; state: "reserved" | "armed" | "released" }>();
|
|
145
|
+
private readonly releasedInputs = new Map<string, ManagedCallIdentity>();
|
|
146
|
+
private retiredPeer = false;
|
|
147
|
+
private snapshotHandler: ((data: unknown) => Promise<SnapshotResult>) | undefined;
|
|
148
|
+
private fileActivity = () => 0;
|
|
149
|
+
private frozen: { readonly operationKey: string; readonly payloadSha: string; readonly paths: readonly string[] } | undefined;
|
|
150
|
+
private snapshot = { definitionSha: "", fingerprint: "" };
|
|
151
|
+
constructor(readonly role: "main" | "agent", private readonly send: (call: CollaborationInvocation) => Promise<unknown>) {}
|
|
152
|
+
installSnapshots(handler: (data: unknown) => Promise<SnapshotResult>, activity: () => number): void { this.snapshotHandler = handler; this.fileActivity = activity; }
|
|
153
|
+
takeSnapshot(data: unknown): Promise<SnapshotResult> {
|
|
154
|
+
return this.snapshotHandler ? this.snapshotHandler(data) : Promise.resolve({ ok: false, error: "snapshot_not_quiescent" });
|
|
155
|
+
}
|
|
156
|
+
start(ctx: unknown): void { this.revoke(); this.runtime = native(ctx); }
|
|
157
|
+
applied(snapshot: { readonly definitionSha: string; readonly fingerprint: string }): void { this.snapshot = Object.freeze({ ...snapshot }); }
|
|
158
|
+
runtimeInfo() {
|
|
159
|
+
const ctx = this.runtime;
|
|
160
|
+
const model = ctx && "model" in ctx && record(ctx.model) ? ctx.model : undefined;
|
|
161
|
+
return { ...this.snapshot, ownedInFlight: this.fileActivity(), model: model && { provider: model.provider, modelId: model.id },
|
|
162
|
+
thinking: ctx && "thinkingLevel" in ctx ? ctx.thinkingLevel : undefined };
|
|
163
|
+
}
|
|
164
|
+
identity() { return this.bound ? { ...this.bound.identity, appliedSha: this.snapshot.definitionSha, snapshotFingerprint: this.snapshot.fingerprint,
|
|
165
|
+
consumed: this.consumed, inputEntryId: this.consumedEntry, ownedInFlight: this.fileActivity() } : null; }
|
|
166
|
+
bind(data: unknown) {
|
|
167
|
+
const runtime = this.runtime;
|
|
168
|
+
if (!runtime || !record(data) || !record(data.binding) || !text(data.secret) || !/^[a-f0-9]{64}$/.test(data.secret) ||
|
|
169
|
+
!(data.requestId === null || text(data.requestId)) || !(data.rootId === null || text(data.rootId))) return fail("invalid_binding");
|
|
170
|
+
const identity = parseManagedCallIdentity(data.identity);
|
|
171
|
+
const reservationId = data.reservationId;
|
|
172
|
+
if (reservationId !== undefined && (!text(reservationId) || this.reservations.has(reservationId))) return fail("binding_conflict");
|
|
173
|
+
const reusable = this.releasedInputs.get(identity.inputId);
|
|
174
|
+
if (reusable && (!text(reservationId) || !isDeepStrictEqual(reusable, identity))) return fail("binding_conflict");
|
|
175
|
+
if (data.inputEvidence !== undefined && data.inputEvidence !== "client_entry") return fail("invalid_binding");
|
|
176
|
+
if (data.inputEvidence === "client_entry" && (this.retiredClientInputs.has(identity.inputId) ||
|
|
177
|
+
this.bound?.identity.inputId === identity.inputId)) return fail("binding_conflict");
|
|
178
|
+
const b = data.binding;
|
|
179
|
+
if (data.inputEvidence === "client_entry" && (identity.activationId !== null || b.readContinuationId !== undefined) &&
|
|
180
|
+
!text(reservationId)) return fail("reservation_required");
|
|
181
|
+
if (reservationId !== undefined && (data.inputEvidence !== "client_entry" ||
|
|
182
|
+
(identity.activationId === null && b.readContinuationId === undefined))) return fail("invalid_binding");
|
|
183
|
+
if (identity.role !== this.role || identity.sessionPath !== runtime.sessionManager.getSessionFile() ||
|
|
184
|
+
identity.durableSessionId !== runtime.sessionManager.getSessionId()) return fail("native_context_mismatch");
|
|
185
|
+
for (const key of ["convId", "agentId", "lifecycleEpoch", "inputId", "activationId", "sessionPath", "durableSessionId", "gatewayGeneration"] as const) {
|
|
186
|
+
if (identity[key] !== b[key]) return fail("binding_conflict");
|
|
187
|
+
}
|
|
188
|
+
if (!text(b.definitionSha) || b.definitionSha !== this.snapshot.definitionSha) return fail("version_conflict");
|
|
189
|
+
if (!integer(b.ownerInputRevision) || (b.readContinuationId !== undefined && !text(b.readContinuationId))) return fail("invalid_binding");
|
|
190
|
+
if (b.readContinuationId !== undefined) {
|
|
191
|
+
if (!record(data.read) || data.read.operation !== "read") return fail("invalid_binding");
|
|
192
|
+
payload("files", data.read);
|
|
193
|
+
}
|
|
194
|
+
const binding: CollaborationBinding = Object.freeze({ convId: identity.convId, agentId: identity.agentId, lifecycleEpoch: identity.lifecycleEpoch,
|
|
195
|
+
inputId: identity.inputId, activationId: identity.activationId, sessionPath: identity.sessionPath, durableSessionId: identity.durableSessionId,
|
|
196
|
+
gatewayGeneration: identity.gatewayGeneration, definitionSha: b.definitionSha, ownerInputRevision: b.ownerInputRevision,
|
|
197
|
+
...(typeof b.readContinuationId === "string" ? { readContinuationId: b.readContinuationId } : {}) });
|
|
198
|
+
this.revoke();
|
|
199
|
+
this.retiredPeer = false;
|
|
200
|
+
this.bound = Object.freeze({ identity, binding, requestId: data.requestId, rootId: data.rootId,
|
|
201
|
+
read: record(data.read) ? Object.freeze(structuredClone(data.read)) : null, secret: createSecretKey(Buffer.from(data.secret, "hex")),
|
|
202
|
+
inputEvidence: data.inputEvidence === "client_entry" ? "client_entry" : "native_event",
|
|
203
|
+
...(text(reservationId) ? { reservationId } : {}) });
|
|
204
|
+
if (text(reservationId)) {
|
|
205
|
+
this.reservations.set(reservationId, { identity, bound: this.bound, state: "reserved" });
|
|
206
|
+
this.releasedInputs.delete(identity.inputId);
|
|
207
|
+
}
|
|
208
|
+
return this.identity();
|
|
209
|
+
}
|
|
210
|
+
/** Only an exact, unarmed reservation can be released. An empty transcript is not enough. */
|
|
211
|
+
reservation(data: unknown) {
|
|
212
|
+
if (!record(data) || !text(data.reservationId) || !this.runtime) return fail("reservation_conflict");
|
|
213
|
+
const saved = this.reservations.get(data.reservationId);
|
|
214
|
+
const identity = parseManagedCallIdentity(data.identity);
|
|
215
|
+
if (!saved || !isDeepStrictEqual(saved.identity, identity) || identity.sessionPath !== this.runtime.sessionManager.getSessionFile() ||
|
|
216
|
+
identity.durableSessionId !== this.runtime.sessionManager.getSessionId()) return fail("reservation_conflict");
|
|
217
|
+
switch (data.action) {
|
|
218
|
+
case "arm":
|
|
219
|
+
if (saved.state !== "reserved" || this.bound !== saved.bound || this.retiredClientInputs.has(identity.inputId)) return fail("reservation_conflict");
|
|
220
|
+
saved.state = "armed";
|
|
221
|
+
return { inputId: identity.inputId, state: saved.state };
|
|
222
|
+
case "release":
|
|
223
|
+
if (saved.state !== "reserved") return fail("reservation_conflict");
|
|
224
|
+
if (this.runtime.sessionManager.getEntries().some(entry => record(entry) && entry.type === "message" &&
|
|
225
|
+
record(entry.message) && entry.message.clientInputId === identity.inputId)) return fail("reservation_conflict");
|
|
226
|
+
if (this.bound === saved.bound) this.revoke();
|
|
227
|
+
saved.state = "released";
|
|
228
|
+
this.releasedInputs.set(identity.inputId, identity);
|
|
229
|
+
this.retiredClientInputs.delete(identity.inputId);
|
|
230
|
+
return { inputId: identity.inputId, state: saved.state };
|
|
231
|
+
default: return fail("reservation_conflict");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
input(event: unknown): boolean {
|
|
235
|
+
if (this.bound?.inputEvidence === "client_entry" && !this.consumed) return this.isPeer();
|
|
236
|
+
if (record(event) && this.bound && event.inputId === this.bound.identity.inputId) { this.consumed = true; return true; }
|
|
237
|
+
this.revoke(); this.retiredPeer = false; return false;
|
|
238
|
+
}
|
|
239
|
+
revoke(): void {
|
|
240
|
+
if (this.bound?.inputEvidence === "client_entry") this.retiredClientInputs.add(this.bound.identity.inputId);
|
|
241
|
+
this.retiredPeer = this.isPeer(); this.bound = undefined; this.consumed = false; this.consumedEntry = undefined; this.frozen = undefined;
|
|
242
|
+
}
|
|
243
|
+
settle(): void { this.revoke(); }
|
|
244
|
+
/** Tool admission runs after native persisted the real input and assistant tool call. */
|
|
245
|
+
observeClientInput(): void {
|
|
246
|
+
const bound = this.bound, runtime = this.runtime;
|
|
247
|
+
if (!bound || bound.inputEvidence !== "client_entry" || !runtime) return;
|
|
248
|
+
const manager = runtime.sessionManager;
|
|
249
|
+
if (!("getLeafId" in manager) || typeof manager.getLeafId !== "function") return fail("native_context_mismatch");
|
|
250
|
+
let cursor: unknown = manager.getLeafId();
|
|
251
|
+
const entries = new Map(manager.getEntries().filter(record).map(entry => [entry.id, entry]));
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
while (text(cursor) && !seen.has(cursor)) {
|
|
254
|
+
seen.add(cursor);
|
|
255
|
+
const entry = entries.get(cursor);
|
|
256
|
+
if (!entry) break;
|
|
257
|
+
if (entry.type === "message" && record(entry.message) && entry.message.role === "user") {
|
|
258
|
+
if (entry.message.clientInputId !== bound.identity.inputId || !text(entry.id)) return fail("native_input_mismatch");
|
|
259
|
+
this.consumed = true; this.consumedEntry = entry.id; return;
|
|
260
|
+
}
|
|
261
|
+
cursor = entry.parentId;
|
|
262
|
+
}
|
|
263
|
+
return fail("native_input_mismatch");
|
|
264
|
+
}
|
|
265
|
+
capture() {
|
|
266
|
+
const bound = this.bound;
|
|
267
|
+
if (!bound || !this.consumed) return fail("binding_missing");
|
|
268
|
+
this.assertReservationArmed();
|
|
269
|
+
return { ...bound, assertCurrent: () => {
|
|
270
|
+
if (this.bound !== bound || !this.consumed) fail("binding_missing");
|
|
271
|
+
}, assertWritable: () => {
|
|
272
|
+
if (this.bound !== bound || !this.consumed) fail("binding_missing");
|
|
273
|
+
if (bound.binding.readContinuationId) fail("read_only_continuation");
|
|
274
|
+
if (this.frozen) fail("request_blocked");
|
|
275
|
+
} };
|
|
276
|
+
}
|
|
277
|
+
captureNative(ctx: unknown) {
|
|
278
|
+
if (native(ctx).sessionManager !== this.runtime?.sessionManager) return fail("native_context_mismatch");
|
|
279
|
+
return this.capture();
|
|
280
|
+
}
|
|
281
|
+
isPeer(): boolean { return this.retiredPeer || Boolean(this.bound?.identity.activationId || this.bound?.binding.readContinuationId); }
|
|
282
|
+
private assertReservationArmed(): void {
|
|
283
|
+
if (this.bound?.reservationId && this.reservations.get(this.bound.reservationId)?.state !== "armed") fail("binding_unarmed");
|
|
284
|
+
}
|
|
285
|
+
assertGeneralTool(): void {
|
|
286
|
+
if (this.retiredPeer && !this.bound) fail("binding_missing");
|
|
287
|
+
this.assertReservationArmed();
|
|
288
|
+
if (this.bound?.binding.readContinuationId) fail("read_only_continuation");
|
|
289
|
+
if (this.frozen) fail("request_blocked");
|
|
290
|
+
}
|
|
291
|
+
snapshotCapture(call: SnapshotCall) {
|
|
292
|
+
const captured = this.capture(), frozen = this.frozen;
|
|
293
|
+
if (!frozen || call.requestId !== captured.requestId || call.operationKey !== frozen.operationKey ||
|
|
294
|
+
call.bindingDigest !== collaborationDigest(captured.binding)) return fail("binding_conflict");
|
|
295
|
+
if (!integer(call.requestRevision) || !isDeepStrictEqual(call.paths, frozen.paths)) return fail("snapshot_conflict");
|
|
296
|
+
if (captured.binding.readContinuationId) return fail("read_only_continuation");
|
|
297
|
+
return captured;
|
|
298
|
+
}
|
|
299
|
+
private clientInvocation(ctx: Native, id: string, operation: Operation): string | undefined {
|
|
300
|
+
const bound = this.bound;
|
|
301
|
+
if (bound?.inputEvidence !== "client_entry") return undefined;
|
|
302
|
+
const manager = ctx.sessionManager;
|
|
303
|
+
if (manager !== this.runtime?.sessionManager || !("getLeafId" in manager) || typeof manager.getLeafId !== "function") return fail("native_context_mismatch");
|
|
304
|
+
let cursor: unknown = manager.getLeafId(), logical: string | undefined;
|
|
305
|
+
const seen = new Set<string>(), entries = new Map(manager.getEntries().filter(record).map(entry => [entry.id, entry]));
|
|
306
|
+
while (text(cursor) && !seen.has(cursor)) {
|
|
307
|
+
seen.add(cursor);
|
|
308
|
+
const entry = entries.get(cursor);
|
|
309
|
+
if (!entry || !text(entry.id)) break;
|
|
310
|
+
const message = entry.message;
|
|
311
|
+
if (entry.type === "message" && record(message)) {
|
|
312
|
+
if (message.role === "user") {
|
|
313
|
+
if (!logical || message.clientInputId !== bound.identity.inputId) return fail("native_input_mismatch");
|
|
314
|
+
this.consumed = true; this.consumedEntry = entry.id;
|
|
315
|
+
return logical;
|
|
316
|
+
}
|
|
317
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
318
|
+
const ordinal = message.content.findIndex((part: unknown) => record(part) && part.type === "toolCall" &&
|
|
319
|
+
part.id === id && part.name === `zgw_agent_${operation}`);
|
|
320
|
+
if (ordinal >= 0 && logical === undefined) logical = JSON.stringify([bound.identity.durableSessionId, entry.id, ordinal]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
cursor = entry.parentId;
|
|
324
|
+
}
|
|
325
|
+
return fail("native_invocation_missing");
|
|
326
|
+
}
|
|
327
|
+
async execute(call: { readonly id: string; readonly params: Record<string, unknown>; readonly signal: AbortSignal | undefined; readonly ctx: unknown; readonly operation: Operation }): Promise<unknown> {
|
|
328
|
+
payload(call.operation, call.params);
|
|
329
|
+
const ctx = native(call.ctx);
|
|
330
|
+
const clientLogical = this.clientInvocation(ctx, call.id, call.operation);
|
|
331
|
+
const capture = this.capture();
|
|
332
|
+
if (call.signal?.aborted || ctx.sessionManager !== this.runtime?.sessionManager) return fail("native_context_mismatch");
|
|
333
|
+
if (capture.binding.readContinuationId && !(call.operation === "files" && isDeepStrictEqual(call.params, capture.read))) return fail("read_only_continuation");
|
|
334
|
+
const operationKey = JSON.stringify([capture.identity.durableSessionId, call.id, call.operation]);
|
|
335
|
+
if (this.frozen && (call.operation !== "result" || operationKey !== this.frozen.operationKey ||
|
|
336
|
+
collaborationDigest(call.params) !== this.frozen.payloadSha)) return fail("request_blocked");
|
|
337
|
+
if (call.operation === "request" && ((this.role === "agent" && (call.params.kind !== "question" || call.params.retryOf !== undefined)) ||
|
|
338
|
+
call.params.agentId === capture.identity.agentId || (capture.identity.activationId !== null && capture.requestId === null))) return fail("request_forbidden");
|
|
339
|
+
if (call.operation === "result" && (capture.requestId === null || call.params.requestId !== capture.requestId)) return fail("binding_conflict");
|
|
340
|
+
let logical = clientLogical;
|
|
341
|
+
for (const entry of clientLogical ? [] : ctx.sessionManager.getEntries()) {
|
|
342
|
+
if (!record(entry) || !text(entry.id) || entry.type !== "message" || !record(entry.message) || entry.message.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
343
|
+
const ordinal = entry.message.content.findIndex((item: unknown) => record(item) && item.type === "toolCall" && item.id === call.id && item.name === `zgw_agent_${call.operation}`);
|
|
344
|
+
if (ordinal >= 0) logical = JSON.stringify([capture.identity.durableSessionId, entry.id, ordinal]);
|
|
345
|
+
}
|
|
346
|
+
if (!logical) return fail("native_invocation_missing");
|
|
347
|
+
const identity = parseManagedCallIdentity({ ...capture.identity, toolCallId: call.id, logicalCallId: logical });
|
|
348
|
+
const operation = call.operation === "files" ? `files.${call.params.operation}` : call.operation;
|
|
349
|
+
let staged: StageFile | undefined;
|
|
350
|
+
if (operation === "files.stage" && typeof call.params.path === "string") {
|
|
351
|
+
if (!this.stageFile) return fail("collaboration_files_not_ready");
|
|
352
|
+
staged = await this.stageFile(call.params.path, ctx, call.signal);
|
|
353
|
+
capture.assertCurrent();
|
|
354
|
+
}
|
|
355
|
+
const envelope = JSON.stringify({ operation, sourceSessionPath: identity.sessionPath, toolCallId: call.id,
|
|
356
|
+
payload: staged ? { ...call.params, path: staged.path } : call.params,
|
|
357
|
+
context: { requestId: capture.requestId, rootId: capture.rootId, binding: capture.binding } });
|
|
358
|
+
let body = new TextEncoder().encode(envelope), route = "/internal/agent-collaboration", contentType = "application/json";
|
|
359
|
+
if (staged) {
|
|
360
|
+
const form = new FormData();
|
|
361
|
+
form.set("metadata", envelope); form.set("file", new Blob([Buffer.from(staged.bytes)], { type: staged.mediaType }), staged.name);
|
|
362
|
+
const encoded = new Request("http://localhost/", { method: "POST", body: form });
|
|
363
|
+
contentType = encoded.headers.get("content-type") ?? fail("invalid_payload");
|
|
364
|
+
body = new Uint8Array(await encoded.arrayBuffer());
|
|
365
|
+
route += "/files";
|
|
366
|
+
}
|
|
367
|
+
let method = "POST";
|
|
368
|
+
if (operation === "files.read" && call.params.view === undefined && typeof call.params.artifactId === "string" && typeof call.params.requestId === "string") {
|
|
369
|
+
method = "GET"; body = new Uint8Array();
|
|
370
|
+
route = `/internal/agent-collaboration/artifacts/${encodeURIComponent(call.params.artifactId)}?requestId=${encodeURIComponent(call.params.requestId)}`;
|
|
371
|
+
}
|
|
372
|
+
const request = { method, route, operation, body };
|
|
373
|
+
const proof = buildManagedCallProof({ native: ctx, nativeInstance: ctx.sessionManager, identity, secret: capture.secret,
|
|
374
|
+
currentIdentity: () => { capture.assertCurrent(); return identity; } }, { ...request, nonce: randomBytes(32).toString("hex") });
|
|
375
|
+
if (call.operation === "result") this.frozen = { operationKey, payloadSha: collaborationDigest(call.params),
|
|
376
|
+
paths: Array.isArray(call.params.outputs) ? call.params.outputs.filter(text) : [] };
|
|
377
|
+
const abort = () => { if (this.bound?.binding === capture.binding) this.revoke(); };
|
|
378
|
+
call.signal?.addEventListener("abort", abort, { once: true });
|
|
379
|
+
try {
|
|
380
|
+
const result = await this.send({ ...request, proof, contentType, ...(call.signal ? { signal: call.signal } : {}) });
|
|
381
|
+
capture.assertCurrent();
|
|
382
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
383
|
+
} finally { call.signal?.removeEventListener("abort", abort); }
|
|
384
|
+
}
|
|
385
|
+
}
|