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,41 @@
|
|
|
1
|
+
import { asideBin } from "./agent-aside.ts";
|
|
2
|
+
import { asideAccount } from "./agent-aside-tool.ts";
|
|
3
|
+
|
|
4
|
+
// 294 D4: the bridge answers the gateway's `zgw_aside_tabs` with the tab list for the owner's picker. It runs the official
|
|
5
|
+
// CLI for listing only (fixed script, argv, no shell); no agent session is involved and no tab is opened or changed.
|
|
6
|
+
|
|
7
|
+
export const TABS_PREFIX = "ZGW_ASIDE_TABS ";
|
|
8
|
+
const TABS_SCRIPT = `const tabs = await listBrowserTabs(); console.log(${JSON.stringify(TABS_PREFIX)} + JSON.stringify(tabs.map((t) => ({ targetId: String(t.targetId), url: String(t.url), title: String(t.title ?? "") }))));`;
|
|
9
|
+
const LIST_TIMEOUT_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
export interface AsideTab { readonly targetId: string; readonly url: string; readonly title: string }
|
|
12
|
+
export type TabsRunner = (bin: string, argv: readonly string[]) => Promise<{ code: number; stdout: string }>;
|
|
13
|
+
|
|
14
|
+
const runTabs: TabsRunner = async (bin, argv) => {
|
|
15
|
+
const child = Bun.spawn([bin, ...argv], { stdin: "ignore", stdout: "pipe", stderr: "ignore", timeout: LIST_TIMEOUT_MS });
|
|
16
|
+
const [stdout, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
|
|
17
|
+
return { code, stdout };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The last prefixed line of the listing; anything that is not a list of string-identified tabs is invalid. */
|
|
21
|
+
export function parseAsideTabs(stdout: string): AsideTab[] | null {
|
|
22
|
+
const line = stdout.split("\n").map((entry) => entry.replace(/\u001b\[[0-9;]*m/g, "").trim()).reverse().find((entry) => entry.startsWith(TABS_PREFIX)) ?? "";
|
|
23
|
+
let value: unknown;
|
|
24
|
+
try { value = JSON.parse(line.slice(TABS_PREFIX.length)); } catch { return null; }
|
|
25
|
+
if (!Array.isArray(value)) return null;
|
|
26
|
+
return value.filter((tab): tab is AsideTab => typeof tab?.targetId === "string" && tab.targetId !== "" && typeof tab.url === "string")
|
|
27
|
+
.map((tab) => ({ targetId: tab.targetId, url: tab.url, title: typeof tab.title === "string" ? tab.title : "" }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** No confirmed account means no listing (the picker answers target_identity_unclear); a failed CLI run is an error. */
|
|
31
|
+
export async function listAsideTabs(deps: { bin?: string; run?: TabsRunner; account?: (bin: string) => Promise<string | null> } = {}):
|
|
32
|
+
Promise<{ account: string | null; tabs: AsideTab[] }> {
|
|
33
|
+
const bin = deps.bin ?? asideBin();
|
|
34
|
+
const account = await (deps.account ?? asideAccount)(bin);
|
|
35
|
+
if (!account) return { account: null, tabs: [] };
|
|
36
|
+
const result = await (deps.run ?? runTabs)(bin, ["repl", "--account", account, TABS_SCRIPT]);
|
|
37
|
+
if (result.code !== 0) throw new Error("aside_failed");
|
|
38
|
+
const tabs = parseAsideTabs(result.stdout);
|
|
39
|
+
if (tabs === null) throw new Error("aside_output_invalid");
|
|
40
|
+
return { account, tabs };
|
|
41
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createSecretKey, randomBytes, randomUUID, type KeyObject } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { parseManagedCallIdentity, type ManagedCallIdentity } from "../../src/agent/managed-call.ts";
|
|
4
|
+
import { attachmentDocumentCall, attachmentReadCall } from "../../src/attachments/managed-binding.ts";
|
|
5
|
+
import { runtimePaths } from "../../src/runtime/paths.ts";
|
|
6
|
+
import { buildManagedCallProof, type ManagedNativeContext } from "./managed-call.ts";
|
|
7
|
+
|
|
8
|
+
export const ATTACHMENT_READ_TOOL = "zgw_attachment_read";
|
|
9
|
+
const ROUTE = "/internal/attachment-read";
|
|
10
|
+
const DOCUMENT_ROUTE = "/internal/attachment-document";
|
|
11
|
+
type NativeContext = ManagedNativeContext & { readonly cwd?: string };
|
|
12
|
+
export interface AttachmentExtension {
|
|
13
|
+
readonly sessionKind: string;
|
|
14
|
+
readonly sessionContext: Readonly<Record<string, string>>;
|
|
15
|
+
readonly rpc: { handle(name: string, handler: (data: unknown) => unknown): void };
|
|
16
|
+
on(event: "session_start" | "session_shutdown", handler: (event: unknown, ctx: NativeContext) => unknown): void;
|
|
17
|
+
/** Present only on an engine that supports document input. */
|
|
18
|
+
setDocumentResolver?(resolve: (request: DocumentResolverRequest) => Promise<unknown>): void;
|
|
19
|
+
registerTool(tool: { name: string; label: string; description: string; parameters: unknown;
|
|
20
|
+
permissionParser(input: Record<string, unknown>): { permission: string; patterns: string[]; always: string[] }[];
|
|
21
|
+
execute(id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: unknown, ctx: NativeContext): Promise<unknown> }): void;
|
|
22
|
+
}
|
|
23
|
+
interface DocumentResolverRequest { readonly reference: { readonly source: { readonly attachmentId: string; readonly grantId: string; readonly originalSha256: string } }; readonly mode: string; readonly sessionId: string; readonly signal?: AbortSignal }
|
|
24
|
+
type BoundIdentity = Omit<ManagedCallIdentity, "inputId" | "toolCallId" | "logicalCallId">;
|
|
25
|
+
const record = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
|
|
27
|
+
/** Registers only for managed main and agent sessions. Work and CLI sessions get no tool and no handler. */
|
|
28
|
+
export function installAttachmentRead(pi: AttachmentExtension): { bound(): boolean } {
|
|
29
|
+
const role = pi.sessionContext.zgw_role, conv = pi.sessionContext.zgw_conv;
|
|
30
|
+
if (pi.sessionKind !== "interactive" || !conv || (role !== "main" && role !== "agent")) return { bound: () => false };
|
|
31
|
+
let manager: ManagedNativeContext["sessionManager"] | undefined;
|
|
32
|
+
let binding: { identity: BoundIdentity; secret: KeyObject; inputId: string | null } | null = null;
|
|
33
|
+
pi.on("session_start", (_event, ctx) => { manager = ctx.sessionManager; binding = null; });
|
|
34
|
+
pi.on("session_shutdown", () => { manager = undefined; binding = null; });
|
|
35
|
+
pi.rpc.handle("zgw.attachment_bind", data => {
|
|
36
|
+
if (!record(data) || typeof data.secret !== "string" || !/^[a-f0-9]{64}$/.test(data.secret) || !record(data.identity) || !manager) throw new Error("attachment_bind_invalid");
|
|
37
|
+
const identity = parseManagedCallIdentity({ ...data.identity, inputId: "pending", toolCallId: "pending", logicalCallId: "pending" });
|
|
38
|
+
if (identity.convId !== conv || identity.role !== role || identity.agentId !== (pi.sessionContext.zgw_agent ?? null) ||
|
|
39
|
+
identity.durableSessionId !== manager.getSessionId() || identity.sessionPath !== manager.getSessionFile()) throw new Error("attachment_bind_mismatch");
|
|
40
|
+
binding = { identity, secret: createSecretKey(Buffer.from(data.secret, "hex")), inputId: typeof data.inputId === "string" ? data.inputId : null };
|
|
41
|
+
return { ok: true, documentResolver: resolverRegistered };
|
|
42
|
+
});
|
|
43
|
+
// Inline bytes are fetched here, per provider request, and never stored in the session. An unbound session gets the unavailable label.
|
|
44
|
+
const resolverRegistered = typeof pi.setDocumentResolver === "function";
|
|
45
|
+
pi.setDocumentResolver?.(async request => {
|
|
46
|
+
const current = binding, native = manager;
|
|
47
|
+
if (!current || !native || current.inputId === null || request.sessionId !== native.getSessionId()) return { status: "attachment_unavailable" };
|
|
48
|
+
const { attachmentId, grantId, originalSha256 } = request.reference.source;
|
|
49
|
+
const body = new TextEncoder().encode(JSON.stringify({ attachmentId, grantId, originalSha256, mode: request.mode }));
|
|
50
|
+
const toolCallId = `document-${randomUUID()}`;
|
|
51
|
+
const identity = parseManagedCallIdentity({ ...current.identity, inputId: current.inputId, toolCallId, logicalCallId: attachmentDocumentCall(toolCallId) });
|
|
52
|
+
const proof = buildManagedCallProof({ native: { sessionManager: native }, nativeInstance: native, identity, secret: current.secret,
|
|
53
|
+
currentIdentity: () => (binding === current && current.inputId === identity.inputId ? identity : null) },
|
|
54
|
+
{ method: "POST", route: DOCUMENT_ROUTE, operation: "attachment.document", body, nonce: randomBytes(32).toString("hex") });
|
|
55
|
+
try {
|
|
56
|
+
const base = process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
57
|
+
const response = await fetch(new URL(DOCUMENT_ROUTE, base), { method: "POST", redirect: "error", body,
|
|
58
|
+
headers: { authorization: `Bearer ${readFileSync(runtimePaths().omoTokenFile, "utf8").trim()}`, "content-type": "application/json",
|
|
59
|
+
"x-zigmini-managed-call": Buffer.from(JSON.stringify(proof)).toString("base64") },
|
|
60
|
+
signal: request.signal ? AbortSignal.any([request.signal, AbortSignal.timeout(120_000)]) : AbortSignal.timeout(120_000) });
|
|
61
|
+
return response.ok ? await response.json() : { status: "attachment_unavailable" };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
64
|
+
return { status: "attachment_unavailable" };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
pi.rpc.handle("zgw.attachment_input", data => {
|
|
68
|
+
if (!binding || !record(data) || data.sessionIncarnation !== binding.identity.sessionIncarnation || typeof data.inputId !== "string") throw new Error("attachment_input_mismatch");
|
|
69
|
+
binding.inputId = data.inputId;
|
|
70
|
+
return { ok: true };
|
|
71
|
+
});
|
|
72
|
+
pi.registerTool({
|
|
73
|
+
name: ATTACHMENT_READ_TOOL, label: "Attachment read",
|
|
74
|
+
// Sending a file is already the owner's permission to read it, so a read preset must not ask again (C9-1).
|
|
75
|
+
permissionParser: input => [{ permission: "read", patterns: [`zgw-attachment:${String(input.attachmentId)}`], always: [] }],
|
|
76
|
+
description: "Read a document the user attached to this conversation. Identify it only by attachmentId from the message metadata. view text returns extracted text; view pages returns rendered pages of a PDF. Continue with nextCursor until it is absent.",
|
|
77
|
+
parameters: { type: "object", additionalProperties: false, required: ["attachmentId", "view"], properties: {
|
|
78
|
+
attachmentId: { type: "string" }, view: { type: "string", enum: ["text", "pages"] },
|
|
79
|
+
cursor: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 2000 } } },
|
|
80
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
81
|
+
const current = binding, native = manager;
|
|
82
|
+
if (!current || !native || current.inputId === null) throw new Error("attachment_read_unbound");
|
|
83
|
+
const body = new TextEncoder().encode(JSON.stringify(params));
|
|
84
|
+
const identity = parseManagedCallIdentity({ ...current.identity, inputId: current.inputId, toolCallId, logicalCallId: attachmentReadCall(toolCallId) });
|
|
85
|
+
const proof = buildManagedCallProof({ native: ctx, nativeInstance: native, identity, secret: current.secret,
|
|
86
|
+
currentIdentity: () => (binding === current && current.inputId === identity.inputId ? identity : null) },
|
|
87
|
+
{ method: "POST", route: ROUTE, operation: "attachment.read", body, nonce: randomBytes(32).toString("hex") });
|
|
88
|
+
const base = process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
89
|
+
const response = await fetch(new URL(ROUTE, base), { method: "POST", redirect: "error", body,
|
|
90
|
+
headers: { authorization: `Bearer ${readFileSync(runtimePaths().omoTokenFile, "utf8").trim()}`, "content-type": "application/json",
|
|
91
|
+
"x-zigmini-managed-call": Buffer.from(JSON.stringify(proof)).toString("base64") },
|
|
92
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(120_000)]) : AbortSignal.timeout(120_000) });
|
|
93
|
+
const result: unknown = await response.json();
|
|
94
|
+
if (!response.ok || !record(result) || !Array.isArray(result.content)) throw new Error(record(result) && typeof result.error === "string" ? result.error : `attachment_read_http_${response.status}`);
|
|
95
|
+
const { content, ...details } = result;
|
|
96
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }, ...content], details };
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return { bound: () => binding !== null };
|
|
100
|
+
}
|
|
@@ -13,6 +13,7 @@ import { bunInstallDir, guardMode, guardStart } from "../../src/engine/schema-gu
|
|
|
13
13
|
import { bridgeMayRun, readRegistry } from "../../src/runtime/slots.ts";
|
|
14
14
|
import { BridgeMaintenance } from "./maintenance.ts";
|
|
15
15
|
import { asciiJson } from "./ascii-json.ts";
|
|
16
|
+
import { listAsideTabs } from "./aside-tabs.ts";
|
|
16
17
|
|
|
17
18
|
const paths = runtimePaths();
|
|
18
19
|
const home = paths.home;
|
|
@@ -506,6 +507,15 @@ const server = Bun.serve<Link>({
|
|
|
506
507
|
}));
|
|
507
508
|
return;
|
|
508
509
|
}
|
|
510
|
+
// 294 D4: the owner's Aside tab picker asks the bridge (token-authenticated link) for the tab list; the CLI lists
|
|
511
|
+
// only and no agent session is involved, so a link without a requester identity is answered too.
|
|
512
|
+
if (record(request) && request.type === "zgw_aside_tabs") {
|
|
513
|
+
const id = request.id;
|
|
514
|
+
const answer = (body: Record<string, unknown>) => deliver(link, JSON.stringify({ type: "response", id, command: "zgw_aside_tabs", ...body }));
|
|
515
|
+
void listAsideTabs().then((data) => answer({ success: true, data }),
|
|
516
|
+
(error: unknown) => answer({ success: false, error: error instanceof Error ? error.message : String(error) }));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
509
519
|
if (record(request) && link.identity) {
|
|
510
520
|
if (request.type === "zgw_transport_info" || request.type === "zgw_transport_resume") {
|
|
511
521
|
if (request.type === "zgw_transport_resume" && !link.ready) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { KeyObject } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ManagedCallError, managedCallBodyDigest, managedCallSignature, parseManagedCallIdentity, sameManagedCallIdentity,
|
|
4
|
+
type ManagedCallIdentity, type ManagedCallProof, type ManagedCallRequest,
|
|
5
|
+
} from "../../src/agent/managed-call.ts";
|
|
6
|
+
|
|
7
|
+
/** Structural SDK port; getters on the real native context enforce runner retirement. */
|
|
8
|
+
export interface ManagedNativeContext {
|
|
9
|
+
readonly sessionManager: {
|
|
10
|
+
getSessionId(): string;
|
|
11
|
+
getSessionFile(): string | undefined;
|
|
12
|
+
};
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Private adapter state, NEVER tool parameters, sessionContext metadata or log data.
|
|
18
|
+
* The trusted bind pins the native manager object and execution identity. Input/tool/
|
|
19
|
+
* logical-call IDs come from the host's actual invocation and operation ledger.
|
|
20
|
+
* Provisioning/Hub identity RPC and retirement wiring are owned by the consumer.
|
|
21
|
+
*/
|
|
22
|
+
export interface ManagedCallContext {
|
|
23
|
+
readonly native: ManagedNativeContext;
|
|
24
|
+
readonly nativeInstance: ManagedNativeContext["sessionManager"];
|
|
25
|
+
readonly identity: ManagedCallIdentity;
|
|
26
|
+
readonly secret: KeyObject;
|
|
27
|
+
/** Current trusted execution identity; null after retirement/cancel/settle. */
|
|
28
|
+
currentIdentity(): ManagedCallIdentity | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildManagedCallProof(
|
|
32
|
+
context: ManagedCallContext,
|
|
33
|
+
request: ManagedCallRequest & { readonly nonce: string },
|
|
34
|
+
): ManagedCallProof {
|
|
35
|
+
// Access the guarded native getter on EVERY call, including retries.
|
|
36
|
+
const manager = context.native.sessionManager;
|
|
37
|
+
const identity = parseManagedCallIdentity(context.identity);
|
|
38
|
+
if (manager !== context.nativeInstance || manager.getSessionId() !== identity.durableSessionId ||
|
|
39
|
+
manager.getSessionFile() !== identity.sessionPath || context.native.signal?.aborted)
|
|
40
|
+
throw new ManagedCallError("native_context_mismatch");
|
|
41
|
+
const current = context.currentIdentity();
|
|
42
|
+
if (current === null || !sameManagedCallIdentity(identity, current)) throw new ManagedCallError("binding_mismatch");
|
|
43
|
+
const content = { nonce: request.nonce, bodySha256: managedCallBodyDigest(request.body) };
|
|
44
|
+
const mac = managedCallSignature({ identity, secret: context.secret }, request, content);
|
|
45
|
+
return Object.freeze({ version: 1, identity, ...content, mac });
|
|
46
|
+
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { registerAgentCollaboration, COLLABORATION_TOOLS, CollaborationExtensionError, type CollaborationControl } from "./agent-collaboration.ts";
|
|
5
|
+
import { CollaborationSnapshots, type NativeFileSdk } from "./agent-collaboration-files.ts";
|
|
6
|
+
import { loadEngineSdk } from "../../src/engine/index.ts";
|
|
2
7
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { runtimePaths } from "../../src/runtime/paths.ts";
|
|
8
|
+
import { runtimeHome, runtimePaths } from "../../src/runtime/paths.ts";
|
|
4
9
|
import { agentRules } from "../../src/agent-rules.ts";
|
|
5
10
|
import { applyRequestRules, loadRequestRules, type ProviderRequestEvent } from "../../src/agent-request-rules.ts";
|
|
6
11
|
import { nicknamePrompt, PROFILE_TIMEOUT_MS, readPromptProfile, type PromptProfile } from "../../src/profile-prompt.ts";
|
|
7
12
|
|
|
8
13
|
// The pinned runtime supplies this SDK surface, just as it does for model-control.ts.
|
|
9
14
|
type Entry = { id: string; type: string; customType?: string; data?: unknown; message?: { role: string; content: unknown; toolName?: string; details?: unknown; toolCallId?: string } };
|
|
10
|
-
type Context = { sessionManager: { getSessionFile(): string | undefined; getEntries(): Entry[] } };
|
|
15
|
+
type Context = { sessionManager: { getSessionFile(): string | undefined; getEntries(): Entry[] }; readonly cwd?: string; readonly model?: unknown; readonly thinkingLevel?: string };
|
|
11
16
|
type Params = Record<string, unknown>;
|
|
12
17
|
type Tool = {
|
|
13
18
|
name: string; label: string; description: string; parameters: unknown;
|
|
@@ -16,11 +21,14 @@ type Tool = {
|
|
|
16
21
|
interface ExtensionApi {
|
|
17
22
|
readonly sessionKind: string;
|
|
18
23
|
readonly sessionContext: Readonly<Record<string, string>>;
|
|
19
|
-
readonly rpc: { handle(name: string, handler: () => unknown): void; emit(name: string, data: unknown): void };
|
|
24
|
+
readonly rpc: { handle(name: string, handler: (data: unknown) => unknown): void; emit(name: string, data: unknown): void };
|
|
20
25
|
readonly events: { on(name: string, handler: (data: unknown) => void): () => void };
|
|
21
26
|
on(event: "session_start", handler: (event: unknown, ctx: Context) => void): void;
|
|
22
27
|
on(event: "before_agent_start", handler: (event: { systemPrompt: string }) => unknown, options: { previewSafe: boolean }): void;
|
|
23
28
|
on(event: "before_provider_request", handler: (event: ProviderRequestEvent) => unknown): void;
|
|
29
|
+
on(event: "input", handler: (event: { inputId: string; text: string }) => unknown): void;
|
|
30
|
+
on(event: "session_abort" | "session_shutdown" | "agent_settled", handler: () => void): void;
|
|
31
|
+
on(event: "tool_call", handler: (event: { toolName: string }) => unknown): void;
|
|
24
32
|
registerTool(tool: Tool): void;
|
|
25
33
|
appendEntry(customType: string, data: unknown): void;
|
|
26
34
|
}
|
|
@@ -127,19 +135,41 @@ async function postWork(operation: string, requestKey: string, sourceSessionPath
|
|
|
127
135
|
return result;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
// allow: SIZE_OK - existing main/work lifecycle and report protocol; the shared file adapter owns the new file logic.
|
|
139
|
+
export default function taskControl(pi: ExtensionApi, loadSdk: () => Promise<NativeFileSdk> = () => loadEngineSdk<NativeFileSdk>()): void {
|
|
131
140
|
const role = pi.sessionContext.zgw_role;
|
|
132
141
|
const workId = pi.sessionContext.zgw_work;
|
|
133
142
|
const conv = pi.sessionContext.zgw_conv;
|
|
134
143
|
let runtime: Context | undefined;
|
|
135
|
-
|
|
144
|
+
let collaboration: CollaborationControl | undefined;
|
|
145
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
146
|
+
runtime = ctx; collaboration?.start(ctx);
|
|
147
|
+
collaboration?.applied({ definitionSha: mainSha, fingerprint: createHash("sha256").update(JSON.stringify({ mainSha, model: ctx.model, thinking: ctx.thinkingLevel })).digest("hex") });
|
|
148
|
+
if (collaboration) {
|
|
149
|
+
if (!ctx.cwd) throw new CollaborationExtensionError("native_cwd_missing");
|
|
150
|
+
const sdk = await loadSdk();
|
|
151
|
+
new CollaborationSnapshots({ root: join(runtimeHome(), "agents"), control: collaboration, queue: sdk.withFileMutationQueue,
|
|
152
|
+
assertKnowledgeLock() { throw new CollaborationExtensionError("read_only_continuation"); } }).install(pi, { sdk, cwd: ctx.cwd });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
136
155
|
pi.rpc.handle("zgw.work_identity", () => ({ role, workId, conv, sessionPath: runtime?.sessionManager.getSessionFile() }));
|
|
137
156
|
const isMain = role === "main" && pi.sessionKind === "interactive" && Boolean(conv);
|
|
138
157
|
const isWork = role === "work" && pi.sessionKind === "worker" && Boolean(workId);
|
|
139
158
|
if (!isMain && !isWork) return; // No task tools in unrelated CLI or speech-summary workers.
|
|
159
|
+
collaboration = isMain ? registerAgentCollaboration(pi) : undefined;
|
|
160
|
+
if (collaboration) {
|
|
161
|
+
pi.on("input", event => { collaboration?.input(event); });
|
|
162
|
+
for (const event of ["session_abort", "session_shutdown", "agent_settled"] as const) pi.on(event, () => { collaboration?.revoke(); });
|
|
163
|
+
pi.on("tool_call", event => {
|
|
164
|
+
try { if (!COLLABORATION_TOOLS.includes(event.toolName)) collaboration?.assertGeneralTool(); }
|
|
165
|
+
catch (error) { if (error instanceof CollaborationExtensionError) return { block: true, reason: error.code }; throw error; }
|
|
166
|
+
if (collaboration?.isPeer() && ["bash", "eval", "task", "zgw_work_report"].includes(event.toolName)) return { block: true, reason: "collaboration_tool_forbidden" };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
140
169
|
const rules = agentRules();
|
|
141
170
|
console.warn(rules.log);
|
|
142
171
|
const rulesText = rules.text === "" ? "" : `\n\n${rules.text}`;
|
|
172
|
+
const mainSha = createHash("sha256").update(rulesText).digest("hex");
|
|
143
173
|
const requestRules = loadRequestRules();
|
|
144
174
|
pi.on("before_provider_request", (event) => applyRequestRules(event, requestRules));
|
|
145
175
|
|
|
@@ -168,6 +198,7 @@ export default function taskControl(pi: ExtensionApi): void {
|
|
|
168
198
|
name: "zgw_precomment", label: "Precomment", description: PRECOMMENT_DESCRIPTION,
|
|
169
199
|
parameters: { type: "object", properties: { text: { type: "string", minLength: 1, maxLength: PRECOMMENT_MAX } }, required: ["text"], additionalProperties: false },
|
|
170
200
|
async execute(toolCallId, params, signal, _update, ctx) {
|
|
201
|
+
collaboration?.assertGeneralTool();
|
|
171
202
|
// A precomment is advisory: a recording failure is reported to the model and never blocks the turn.
|
|
172
203
|
try {
|
|
173
204
|
const text = precommentText(params.text);
|
|
@@ -189,6 +220,7 @@ export default function taskControl(pi: ExtensionApi): void {
|
|
|
189
220
|
description: "List the user's agents read-only: name, one-line description and the web link of each agent's conversation. Use it to answer which agents exist or to give the user the link to open one. Returns no agent knowledge.",
|
|
190
221
|
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
191
222
|
async execute(_toolCallId, _params, signal) {
|
|
223
|
+
collaboration?.assertGeneralTool();
|
|
192
224
|
const token = readFileSync(runtimePaths().omoTokenFile, "utf8").trim();
|
|
193
225
|
const response = await fetch(new URL("/internal/agent-list", gatewayBase()), {
|
|
194
226
|
headers: { authorization: `Bearer ${token}` },
|
|
@@ -212,11 +244,13 @@ export default function taskControl(pi: ExtensionApi): void {
|
|
|
212
244
|
name: `zgw_work_${operation}`, label: `Work ${operation}`, description: spec.description,
|
|
213
245
|
parameters: { type: "object", properties: spec.fields, required: spec.required, additionalProperties: false },
|
|
214
246
|
async execute(toolCallId, params, signal, _update, ctx) {
|
|
247
|
+
collaboration?.assertGeneralTool();
|
|
215
248
|
const sourceSessionPath = ctx.sessionManager.getSessionFile();
|
|
216
249
|
if (!sourceSessionPath) throw new Error("work_source_session_missing");
|
|
217
250
|
const requestKey = JSON.stringify([sourceSessionPath, toolCallId]);
|
|
218
251
|
let payload: Params = params;
|
|
219
252
|
if (operation === "report") {
|
|
253
|
+
if (isMain && collaboration?.isPeer()) throw new Error("collaboration_owner_answer_forbidden");
|
|
220
254
|
if (isMain && (params.kind !== "question" ||
|
|
221
255
|
Object.keys(params).some((key) => !["kind", "summary", "details"].includes(key)))) {
|
|
222
256
|
throw new Error("main_question_only");
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Unreleased attachment 293 engine candidate
|
|
2
|
+
|
|
3
|
+
This directory is a test candidate, not a release registration. The released
|
|
4
|
+
manifest, existing patches, compatibility pins, installer and build-ID code are
|
|
5
|
+
unchanged. Revision 4 is only the existing builder's nested directory label under
|
|
6
|
+
a task-owned, manifest-hashed root. Never install this candidate over a published
|
|
7
|
+
or installed engine.
|
|
8
|
+
|
|
9
|
+
## Reproduction
|
|
10
|
+
|
|
11
|
+
`test/fixtures/attachments/engine/candidate.ts` installs the stock
|
|
12
|
+
`omo-ai@5.1.51` package with the existing `installStock` (npm registry, or
|
|
13
|
+
`ZIGMINI_REGISTRY_URL`) as its input. It reads nothing from `.omo/evidence`
|
|
14
|
+
(bug 304), so a fresh clone, CI and another Mac build the same tree. It stages this manifest, the
|
|
15
|
+
unchanged baseline patches and the document patch, then calls the existing
|
|
16
|
+
`buildPatched(version, omoDir, patchDir)` with both directories explicit. The
|
|
17
|
+
builder checks fixed original and patched hashes. It never derives expected
|
|
18
|
+
hashes from the candidate it is checking.
|
|
19
|
+
|
|
20
|
+
The output is `<cache>/<manifest-sha>/omo/5.1.51-zgw.4`, where `<cache>` is
|
|
21
|
+
`ATTACHMENT_ENGINE_CACHE` or `zgw-attachment-engine` in the OS temp folder.
|
|
22
|
+
Importing `candidate-ready.ts` (or awaiting `buildCandidate()`) builds the
|
|
23
|
+
candidate once; later runs verify the fixed patched hashes. Importing
|
|
24
|
+
`candidate.ts` alone has no side effect. `ATTACHMENT_ENGINE_ROOT` uses an already built candidate instead.
|
|
25
|
+
Without network access and without that variable, the engine tests fail; they
|
|
26
|
+
are never skipped.
|
|
27
|
+
The source snippets in `document-content.js` and `document-content.types.txt`
|
|
28
|
+
are included in the frozen unified patch, not imported from this directory at
|
|
29
|
+
runtime. Changing a snippet requires updating that patch and its fixed manifest
|
|
30
|
+
hashes together.
|
|
31
|
+
|
|
32
|
+
From the task worktree:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
env -i HOME="$HOME" PATH="$PATH" USER="$USER" LOGNAME="$LOGNAME" \
|
|
36
|
+
SHELL="$SHELL" TMPDIR="${TMPDIR:-/tmp}" LANG=en_US.UTF-8 \
|
|
37
|
+
bun run heavy omo-check -- perl -e 'alarm shift; exec @ARGV' 600 \
|
|
38
|
+
bun test/fixtures/attachments/engine/run.ts \
|
|
39
|
+
bun test/fixtures/attachments/engine/candidate.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use the same clean wrapper with the `test` slot for
|
|
43
|
+
`bun test test/contract/attachment-engine.contract.test.ts` and
|
|
44
|
+
`bun test test/unit/attachment-engine.test.ts`.
|
|
45
|
+
Use the separate `mutation` slot for
|
|
46
|
+
`bun test test/mutation/attachment-engine.test.ts`.
|
|
47
|
+
`ATTACHMENT_MUTATION_SEED` can reproduce a recorded seed when set after slot
|
|
48
|
+
acquisition. Normal mutation runs choose a new seed.
|
|
49
|
+
`fingerprint.ts` hashes the complete candidate tree and rejects escaping links.
|
|
50
|
+
|
|
51
|
+
## Engine boundary
|
|
52
|
+
|
|
53
|
+
RPC advertises `document_content_v1`. `prompt`, `steer` and `follow_up` accept the
|
|
54
|
+
approved PDF `DocumentContent`, owner metadata and client input identity.
|
|
55
|
+
Ingress checks PDF magic, original digest, size and exact attachment/grant
|
|
56
|
+
correlation. The persisted input is a `DocumentReference`; base64 is not stored
|
|
57
|
+
in admission, queue or transcript records. Compaction checkpoints retain
|
|
58
|
+
references and owner/input provenance even after the resident mirror is trimmed.
|
|
59
|
+
`append_session_entry` also accepts validated owner-document history for native
|
|
60
|
+
session replacement. Bare `append_user_message` cannot invent document
|
|
61
|
+
provenance; it remains text/image-only.
|
|
62
|
+
|
|
63
|
+
The gateway extension registers the resolver with
|
|
64
|
+
`pi.setDocumentResolver(resolve)`, either while the extension loads or later.
|
|
65
|
+
Only one extension owns it per session; another owner gets
|
|
66
|
+
`document_resolver_already_registered`. An in-process host may call
|
|
67
|
+
`AgentSession.setDocumentResolver(resolve)` directly.
|
|
68
|
+
The resolver receives the current native session/input identity, the originating
|
|
69
|
+
input ID, exact reference, selected model, mode and cancellation signal. It must
|
|
70
|
+
use the verified managed binding and active grant ledger. A filename, attachment
|
|
71
|
+
ID or callback registration is not authentication. No resolver means a typed
|
|
72
|
+
unavailable label; there is no implicit filesystem reader.
|
|
73
|
+
|
|
74
|
+
A ready resolver reply carries:
|
|
75
|
+
|
|
76
|
+
- Original SHA, the reader's **unchanged** representation SHA and grant revision.
|
|
77
|
+
- `payloadSha256`: SHA256 of UTF-8
|
|
78
|
+
`JSON.stringify({content, coverage, ...(nextCursor === undefined ? {} : {nextCursor})})`.
|
|
79
|
+
This delivery-envelope digest is separate from the native reader's complete
|
|
80
|
+
prepared-text digest. T4's digest and cursor contracts are not redefined.
|
|
81
|
+
- The validated representation, coverage, optional opaque continuation and
|
|
82
|
+
a conservative input-token estimate from the preparing service.
|
|
83
|
+
|
|
84
|
+
The native helper's internal scalar cursor is not this engine's opaque cursor.
|
|
85
|
+
T6 must translate it through its grant-bound cursor service and recheck the
|
|
86
|
+
reader's original/representation digest before constructing a delivery envelope.
|
|
87
|
+
The engine does not issue grants, inspect text-security holds, or implement HMAC.
|
|
88
|
+
|
|
89
|
+
`ready` is the resolver's complete-preparation prerequisite. `coverage` describes
|
|
90
|
+
only the current bounded delivery, not all prepared pages. A partial initial
|
|
91
|
+
delivery requires `nextCursor`, including OCR. The engine retains that opaque
|
|
92
|
+
cursor with the exact source/grant, representation digest and grant revision;
|
|
93
|
+
it does not fetch or concatenate subsequent chunks. Subsequent
|
|
94
|
+
`zgw_attachment_read` replies remain bounded text/image tool results, with T6
|
|
95
|
+
responsible for their cursor scope, range completion and release checks.
|
|
96
|
+
Non-native delivered `rendered-image` pages require exactly one actual image
|
|
97
|
+
block per declared page. Text coverage requires actual text that isn't only
|
|
98
|
+
whitespace after trimming, and a truly empty page uses `verified-blank`. The
|
|
99
|
+
aggregate text content is bounded to 48 KiB in UTF-8, not JavaScript characters.
|
|
100
|
+
|
|
101
|
+
Public OpenAI Responses and Anthropic Messages use top-level native PDF blocks.
|
|
102
|
+
Subscription requests use prepared text/images; text-only models use prepared
|
|
103
|
+
OCR text. Requests reselect their representation, including after reopen,
|
|
104
|
+
compaction and a provider change. Revoked/unavailable replies become typed
|
|
105
|
+
labels. `attachment_text_held` stops the request without content.
|
|
106
|
+
|
|
107
|
+
The engine bounds original bytes, base64, coverage, image count/bytes and
|
|
108
|
+
serialized requests. Native size/page/context overflow selects pages before a
|
|
109
|
+
provider dispatch where that representation fits. Provider errors carrying
|
|
110
|
+
document input do not retry an uncertain request. Claude document requests use a
|
|
111
|
+
fresh cold seed with all authorized document page images, rather than the old
|
|
112
|
+
eight-image history elision.
|
|
113
|
+
|
|
114
|
+
## Evidence boundaries
|
|
115
|
+
|
|
116
|
+
The contracts execute the candidate's native CLI runtime, RPC handler, durable
|
|
117
|
+
queue/transcript and compaction pipeline. Public and ChatGPT adapters use real
|
|
118
|
+
loopback HTTP with scripted model replies. Claude's actual adapter is exercised
|
|
119
|
+
at its SDK query/prompt boundary with scripted SDK replies; this is not proof of
|
|
120
|
+
Claude SDK-internal HTTP framing or real-model comprehension.
|
|
121
|
+
|
|
122
|
+
Prepared page/OCR inputs in these engine fixtures are synthetic representation
|
|
123
|
+
inputs. They are not evidence of PDFKit rendering or Vision recognition. Those
|
|
124
|
+
native operations remain T4's proof. Managed-call authentication and rotation
|
|
125
|
+
remain 292 T4-owned. The fixture's reopen/compaction checks do not claim that
|
|
126
|
+
unimplemented authentication integration ran. Gateway/T6 registration and final
|
|
127
|
+
combined-channel tests remain separate.
|
|
128
|
+
|
|
129
|
+
Primary provider documents read on 2026-11-06:
|
|
130
|
+
|
|
131
|
+
- `https://developers.openai.com/api/docs/guides/file-inputs`
|
|
132
|
+
- `https://platform.claude.com/docs/en/build-with-claude/pdf-support`
|
|
133
|
+
|
|
134
|
+
The local OpenAI serialized ceiling is 50,000,000 bytes, stricter than the
|
|
135
|
+
documented aggregate decoded-file limit after base64 expansion. The public
|
|
136
|
+
Anthropic ceiling is 128,000,000 bytes. Subscription input uses a conservative
|
|
137
|
+
24 MiB engine-request ceiling. These are payload checks, not live semantic
|
|
138
|
+
verification or authorization to call configured providers.
|