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,282 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { AgentContentError, AgentContentFiles, type ContentVersion } from "../../src/agent/file-core.ts";
|
|
4
|
+
import { collaborationDigest } from "../../src/agent/collaboration-types.ts";
|
|
5
|
+
import { sniffImage } from "../../src/media.ts";
|
|
6
|
+
import { CollaborationExtensionError, record, type CollaborationApi, type CollaborationControl, type CollaborationTool, type SealedOutput, type SnapshotCall, type SnapshotError, type SnapshotResult } from "./agent-collaboration.ts";
|
|
7
|
+
|
|
8
|
+
type FileOperations = {
|
|
9
|
+
readonly access: (path: string) => Promise<void>; readonly readFile: (path: string) => Promise<Buffer>;
|
|
10
|
+
readonly writeFile: (path: string, content: string) => Promise<void>; readonly mkdir: (path: string) => Promise<void>;
|
|
11
|
+
readonly detectImageMimeType: (path: string) => Promise<string | undefined>;
|
|
12
|
+
};
|
|
13
|
+
export type FilePolicy = (request: { readonly operation: string; readonly canonicalPath: string }) => Promise<{ allow: true } | { allow: false; reason: string }>;
|
|
14
|
+
type FileOptions = { readonly operations?: FileOperations; readonly filesystemPolicy?: FilePolicy };
|
|
15
|
+
type ReadOptions = {
|
|
16
|
+
readonly operations?: Pick<FileOperations, "access" | "readFile"> & {
|
|
17
|
+
readonly detectImageMimeType?: (path: string) => Promise<string | null | undefined>;
|
|
18
|
+
};
|
|
19
|
+
readonly filesystemPolicy?: FilePolicy;
|
|
20
|
+
};
|
|
21
|
+
export interface NativeFileSdk {
|
|
22
|
+
createReadToolDefinition(cwd: string, options?: ReadOptions): CollaborationTool;
|
|
23
|
+
createWriteToolDefinition(cwd: string, options?: FileOptions): CollaborationTool;
|
|
24
|
+
createEditToolDefinition(cwd: string, options?: FileOptions): CollaborationTool;
|
|
25
|
+
createReadOnlyTools(cwd: string, options?: { readonly read?: ReadOptions }): readonly CollaborationTool[];
|
|
26
|
+
withFileMutationQueue<T>(path: string, action: () => Promise<T>): Promise<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type SnapshotRuntime = {
|
|
30
|
+
readonly root: string;
|
|
31
|
+
readonly control: CollaborationControl;
|
|
32
|
+
/** Pass the public queue from the same SDK instance as the registered native definitions. */
|
|
33
|
+
readonly queue: <T>(path: string, action: () => Promise<T>) => Promise<T>;
|
|
34
|
+
readonly assertKnowledgeLock: () => void;
|
|
35
|
+
};
|
|
36
|
+
const fail = (code: SnapshotError): never => { throw new CollaborationExtensionError(code); };
|
|
37
|
+
const digest = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
38
|
+
const segment = (value: string): boolean => value.length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value);
|
|
39
|
+
function parseCall(value: unknown): SnapshotCall {
|
|
40
|
+
if (!record(value) || Object.keys(value).some(key => !["requestId", "requestRevision", "operationKey", "bindingDigest", "paths"].includes(key)) ||
|
|
41
|
+
typeof value.requestId !== "string" || !segment(value.requestId) || typeof value.operationKey !== "string" || !value.operationKey ||
|
|
42
|
+
typeof value.bindingDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.bindingDigest) ||
|
|
43
|
+
typeof value.requestRevision !== "number" || !Number.isSafeInteger(value.requestRevision) || value.requestRevision < 0 ||
|
|
44
|
+
!Array.isArray(value.paths) || value.paths.length > 10 ||
|
|
45
|
+
!value.paths.every((path): path is string => typeof path === "string" && path.startsWith("outputs/") && path.split("/").every(segment))) return fail("invalid_snapshot");
|
|
46
|
+
const paths = [...value.paths];
|
|
47
|
+
if (new Set(paths.map(path => path.normalize("NFC").toLowerCase())).size !== paths.length) return fail("invalid_snapshot");
|
|
48
|
+
return { requestId: value.requestId, requestRevision: value.requestRevision, operationKey: value.operationKey, bindingDigest: value.bindingDigest, paths };
|
|
49
|
+
}
|
|
50
|
+
function sealed(value: unknown): value is SealedOutput {
|
|
51
|
+
return record(value) && typeof value.path === "string" && typeof value.sealedName === "string" &&
|
|
52
|
+
/^[a-f0-9-]{36}$/.test(value.sealedName) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256) &&
|
|
53
|
+
typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0 && value.bytes <= 10 * 1024 * 1024 &&
|
|
54
|
+
typeof value.mediaType === "string" && typeof value.name === "string";
|
|
55
|
+
}
|
|
56
|
+
function errorCode(error: unknown): SnapshotError {
|
|
57
|
+
if (error instanceof SyntaxError) return "snapshot_unknown";
|
|
58
|
+
if (error instanceof CollaborationExtensionError) {
|
|
59
|
+
switch (error.code) {
|
|
60
|
+
case "invalid_snapshot": case "snapshot_conflict": case "binding_conflict": case "request_blocked":
|
|
61
|
+
case "read_only_continuation": case "agent_knowledge_locked": case "snapshot_not_quiescent":
|
|
62
|
+
case "file_not_found": case "file_changed": case "file_not_regular": case "file_linked":
|
|
63
|
+
case "path_forbidden": case "file_too_large": case "snapshot_io_failed": case "snapshot_unknown": return error.code;
|
|
64
|
+
case "binding_missing": return "binding_conflict";
|
|
65
|
+
default: throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (error instanceof AgentContentError) {
|
|
69
|
+
switch (error.code) {
|
|
70
|
+
case "file_changed": return "file_changed";
|
|
71
|
+
case "not_single_regular_file": return "file_not_regular";
|
|
72
|
+
case "too_large": return "file_too_large";
|
|
73
|
+
case "invalid_path": case "invalid_filename": case "unsafe_root": return "path_forbidden";
|
|
74
|
+
case "unsafe_path": return error.errno === 2 ? "file_not_found" : "path_forbidden";
|
|
75
|
+
case "file_exists": return "snapshot_conflict";
|
|
76
|
+
default: return "snapshot_io_failed";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// allow: SIZE_OK - T5 path-stage reuses this T4-owned SDK queue and policy closure; unrelated snapshot/writer extraction is forbidden.
|
|
83
|
+
/** Actual T3 bytes and immutable receipts. SDK writer installation remains the caller's separate obligation. */
|
|
84
|
+
export class CollaborationSnapshots {
|
|
85
|
+
private readonly running = new Map<string, { readonly payload: string; readonly promise: Promise<SnapshotResult> }>();
|
|
86
|
+
private readonly operations = new Set<Promise<unknown>>();
|
|
87
|
+
constructor(private readonly runtime: SnapshotRuntime) {
|
|
88
|
+
runtime.control.installSnapshots(data => this.snapshot(data), () => this.operations.size);
|
|
89
|
+
}
|
|
90
|
+
/** Count the actual operation promise, never a tool_call/tool_result interval or an early abort rejection. */
|
|
91
|
+
async track<T>(operation: (assertWritable: () => void) => Promise<T>): Promise<T> {
|
|
92
|
+
const captured = this.runtime.control.capture();
|
|
93
|
+
captured.assertWritable();
|
|
94
|
+
const running = operation(captured.assertWritable);
|
|
95
|
+
this.operations.add(running);
|
|
96
|
+
try { return await running; } finally { this.operations.delete(running); }
|
|
97
|
+
}
|
|
98
|
+
/** The logical outputs area never falls back to agent-wide public outputs. */
|
|
99
|
+
outputRoot(): string | undefined {
|
|
100
|
+
const captured = this.runtime.control.capture();
|
|
101
|
+
if (!captured.requestId || !captured.binding.agentId || captured.binding.readContinuationId) return undefined;
|
|
102
|
+
if (![captured.requestId, captured.binding.agentId].every(segment)) return fail("path_forbidden");
|
|
103
|
+
return join(this.runtime.root, ".collaboration", "work", captured.binding.agentId, String(captured.binding.lifecycleEpoch), captured.requestId);
|
|
104
|
+
}
|
|
105
|
+
install(api: CollaborationApi, input: { readonly sdk: NativeFileSdk; readonly cwd: string; readonly policy?: FilePolicy }): void {
|
|
106
|
+
const { sdk, cwd, policy } = input;
|
|
107
|
+
this.runtime.control.installStage(async (path, ctx, signal) => {
|
|
108
|
+
const captured = this.runtime.control.captureNative(ctx);
|
|
109
|
+
captured.assertWritable();
|
|
110
|
+
if (path.includes("\0") || path.split(/[\\/]/).some(part => part === ".." || part === ".collaboration" || part === ".file-trash" || part === ".trash")) return fail("path_forbidden");
|
|
111
|
+
const logical = relative(cwd, resolve(cwd, path)), parts = logical.split("/");
|
|
112
|
+
if (captured.binding.agentId !== null && (!parts.every(segment) || !["source", "outputs"].includes(parts[0] ?? ""))) return fail("path_forbidden");
|
|
113
|
+
const privateRoot = captured.binding.activationId !== null && parts[0] === "outputs" ? this.outputRoot() : undefined;
|
|
114
|
+
if (captured.binding.activationId !== null && parts[0] === "outputs" && !privateRoot) return fail("path_forbidden");
|
|
115
|
+
const nativeCwd = privateRoot ?? cwd, target = resolve(nativeCwd, captured.binding.agentId === null ? path : logical);
|
|
116
|
+
const fence = () => {
|
|
117
|
+
captured.assertWritable();
|
|
118
|
+
if (signal?.aborted) return fail("binding_conflict");
|
|
119
|
+
if (captured.binding.agentId !== null) this.runtime.assertKnowledgeLock();
|
|
120
|
+
};
|
|
121
|
+
// The same native reader/policy admits the path. Capture its actual bytes, not its truncated display.
|
|
122
|
+
return this.track(async () => {
|
|
123
|
+
const supplied = await sdk.withFileMutationQueue(target, async () => {
|
|
124
|
+
fence();
|
|
125
|
+
using core = new AgentContentFiles(dirname(target));
|
|
126
|
+
let bytes: Buffer | undefined;
|
|
127
|
+
const load = (actual: string): Buffer => {
|
|
128
|
+
fence(); if (actual !== target) return fail("path_forbidden");
|
|
129
|
+
bytes ??= core.read([target.slice(dirname(target).length + 1)]);
|
|
130
|
+
return bytes;
|
|
131
|
+
};
|
|
132
|
+
const reader = sdk.createReadOnlyTools(nativeCwd, { read: {
|
|
133
|
+
...(policy ? { filesystemPolicy: async (request: Parameters<FilePolicy>[0]) => {
|
|
134
|
+
fence();
|
|
135
|
+
if (request.canonicalPath !== target) return { allow: false as const, reason: "path_forbidden" };
|
|
136
|
+
return policy({ ...request, canonicalPath: privateRoot ? resolve(cwd, logical) : request.canonicalPath });
|
|
137
|
+
} } : {}),
|
|
138
|
+
operations: { access: async actual => { load(actual); }, readFile: async actual => load(actual),
|
|
139
|
+
detectImageMimeType: async actual => sniffImage(load(actual))?.type },
|
|
140
|
+
} }).find(tool => tool.name === "read");
|
|
141
|
+
if (!reader) throw new CollaborationExtensionError("native_read_definition_missing");
|
|
142
|
+
await reader.execute("collaboration-stage", { path: target }, undefined, undefined, ctx);
|
|
143
|
+
fence();
|
|
144
|
+
if (!bytes) return fail("file_not_found");
|
|
145
|
+
return bytes;
|
|
146
|
+
});
|
|
147
|
+
fence();
|
|
148
|
+
return { bytes: supplied, path: captured.binding.agentId === null ? path : logical,
|
|
149
|
+
name: target.slice(dirname(target).length + 1), mediaType: sniffImage(supplied)?.type ?? "application/octet-stream" };
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const read = (root: string, options?: ReadOptions): CollaborationTool => {
|
|
153
|
+
// The public composed factory preserves the SDK's selectedReadFolder default as well as its native reader.
|
|
154
|
+
const tool = sdk.createReadOnlyTools(root, { read: options }).find(tool => tool.name === "read");
|
|
155
|
+
if (!tool) throw new CollaborationExtensionError("native_read_definition_missing");
|
|
156
|
+
return { ...sdk.createReadToolDefinition(root, options), execute: tool.execute };
|
|
157
|
+
};
|
|
158
|
+
const factories = { read, write: sdk.createWriteToolDefinition, edit: sdk.createEditToolDefinition };
|
|
159
|
+
for (const kind of ["read", "write", "edit"] as const) {
|
|
160
|
+
const factory = factories[kind], ordinary = factory(cwd, policy ? { filesystemPolicy: policy } : undefined);
|
|
161
|
+
api.registerTool({ ...ordinary, serverPinned: true, execute: async (id, params, signal, update, ctx) => {
|
|
162
|
+
if (!this.runtime.control.isPeer()) return ordinary.execute(id, params, signal, update, ctx);
|
|
163
|
+
const captured = this.runtime.control.captureNative(ctx);
|
|
164
|
+
captured.assertWritable();
|
|
165
|
+
if (!record(ctx) || !captured.binding.agentId || !captured.requestId) return fail("read_only_continuation");
|
|
166
|
+
if (typeof params.path !== "string" || params.path.includes("\0") || params.path.split(/[\\/]/).some(part => part === "..")) return fail("path_forbidden");
|
|
167
|
+
const logical = relative(cwd, resolve(cwd, params.path));
|
|
168
|
+
const segments = logical.split("/");
|
|
169
|
+
if (isAbsolute(logical) || !segments.every(segment)) return fail("path_forbidden");
|
|
170
|
+
const output = segments[0] === "outputs";
|
|
171
|
+
if (!output && (kind !== "read" || !["source", "notes", "MEMORY.md"].includes(segments[0] ?? ""))) return fail("path_forbidden");
|
|
172
|
+
const privateRoot = this.outputRoot();
|
|
173
|
+
if (!privateRoot) return fail("read_only_continuation");
|
|
174
|
+
const nativeCwd = output ? privateRoot : cwd, target = join(nativeCwd, logical);
|
|
175
|
+
const parts = relative(this.runtime.root, target).split("/");
|
|
176
|
+
if (!parts.every(segment)) return fail("path_forbidden");
|
|
177
|
+
const fence = () => {
|
|
178
|
+
captured.assertWritable();
|
|
179
|
+
if (signal?.aborted) return fail("binding_conflict");
|
|
180
|
+
this.runtime.assertKnowledgeLock();
|
|
181
|
+
};
|
|
182
|
+
return this.track(async () => {
|
|
183
|
+
using core = new AgentContentFiles(this.runtime.root);
|
|
184
|
+
let loaded: { readonly bytes: Buffer; readonly identity: ContentVersion } | undefined;
|
|
185
|
+
const exact = (path: string) => { fence(); if (path !== target) fail("path_forbidden"); };
|
|
186
|
+
const operations: FileOperations = {
|
|
187
|
+
access: async path => { exact(path); loaded = core.readWithIdentity(parts); },
|
|
188
|
+
readFile: async path => { exact(path); loaded ??= core.readWithIdentity(parts); return loaded.bytes; },
|
|
189
|
+
detectImageMimeType: async path => { exact(path); loaded ??= core.readWithIdentity(parts); return sniffImage(loaded.bytes)?.type; },
|
|
190
|
+
// Native mkdir admission is checked here; the descriptor core creates missing directories with its write fence.
|
|
191
|
+
mkdir: async path => { fence(); if (path !== dirname(target)) fail("path_forbidden"); },
|
|
192
|
+
writeFile: async (path, content) => {
|
|
193
|
+
exact(path);
|
|
194
|
+
core.write({ segments: parts, bytes: Buffer.from(content),
|
|
195
|
+
...(loaded ? { expectedIdentity: loaded.identity } : {}), beforeMutation: fence });
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
const tool = factory(nativeCwd, { operations });
|
|
199
|
+
// Preserve all native getters, updates and render/result behavior; only this invocation's cwd changes.
|
|
200
|
+
const nativeCtx = new Proxy(ctx, { get: (object, key, receiver) => key === "cwd" ? nativeCwd : Reflect.get(object, key, receiver) });
|
|
201
|
+
// A native read can reject early on abort while its image/helper still runs. Drain the real promise instead.
|
|
202
|
+
const result = await tool.execute(id, { ...params, path: logical }, kind === "read" ? undefined : signal, update, nativeCtx);
|
|
203
|
+
fence();
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
} });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async snapshot(data: unknown): Promise<SnapshotResult> {
|
|
210
|
+
try {
|
|
211
|
+
const call = parseCall(data);
|
|
212
|
+
const captured = this.runtime.control.snapshotCapture(call);
|
|
213
|
+
const key = collaborationDigest([call.bindingDigest, call.operationKey]), payload = collaborationDigest(call);
|
|
214
|
+
const existing = this.running.get(key);
|
|
215
|
+
if (existing) return existing.payload === payload ? existing.promise : { ok: false, error: "snapshot_conflict" };
|
|
216
|
+
const promise = this.capture({ call, key, payload, assertCurrent: captured.assertCurrent, agentId: captured.binding.agentId,
|
|
217
|
+
lifecycleEpoch: captured.binding.lifecycleEpoch }).catch((error: unknown): SnapshotResult => ({ ok: false, error: errorCode(error) }));
|
|
218
|
+
this.running.set(key, { payload, promise });
|
|
219
|
+
try { return await promise; } finally { this.running.delete(key); }
|
|
220
|
+
} catch (error) { return { ok: false, error: errorCode(error) }; }
|
|
221
|
+
}
|
|
222
|
+
private async capture(input: { readonly call: SnapshotCall; readonly key: string; readonly payload: string; readonly assertCurrent: () => void;
|
|
223
|
+
readonly agentId: string | null; readonly lifecycleEpoch: number }): Promise<SnapshotResult> {
|
|
224
|
+
// Freeze belongs to the exact registered result execute. Drain happens before taking any file queue.
|
|
225
|
+
await Promise.allSettled([...this.operations]);
|
|
226
|
+
input.assertCurrent();
|
|
227
|
+
this.runtime.assertKnowledgeLock();
|
|
228
|
+
if (input.agentId === null || !segment(input.agentId)) return fail("binding_conflict");
|
|
229
|
+
using core = new AgentContentFiles(this.runtime.root);
|
|
230
|
+
const receipt = [".collaboration", "snapshots", input.key];
|
|
231
|
+
try {
|
|
232
|
+
core.publish({ directory: receipt, name: "claim.json", bytes: Buffer.from(JSON.stringify({ payload: input.payload })),
|
|
233
|
+
immutable: true, exact: true });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (!(error instanceof AgentContentError) || error.code !== "file_exists") throw error;
|
|
236
|
+
const claim: unknown = JSON.parse(core.read([...receipt, "claim.json"]).toString());
|
|
237
|
+
if (!record(claim) || claim.payload !== input.payload) return fail("snapshot_conflict");
|
|
238
|
+
let completed: unknown;
|
|
239
|
+
try { completed = JSON.parse(core.read([...receipt, "complete.json"]).toString()); }
|
|
240
|
+
catch (readError) {
|
|
241
|
+
if (readError instanceof AgentContentError && readError.errno === 2) return fail("snapshot_unknown");
|
|
242
|
+
throw readError;
|
|
243
|
+
}
|
|
244
|
+
if (!record(completed) || completed.ok !== true || completed.snapshotKey !== input.key ||
|
|
245
|
+
completed.requestId !== input.call.requestId || completed.bindingDigest !== input.call.bindingDigest ||
|
|
246
|
+
!Array.isArray(completed.files) || !completed.files.every(sealed) ||
|
|
247
|
+
completed.files.length !== input.call.paths.length ||
|
|
248
|
+
completed.files.some((file, index) => file.path !== input.call.paths[index])) return fail("snapshot_unknown");
|
|
249
|
+
for (const file of completed.files) {
|
|
250
|
+
const bytes = core.read([".collaboration", "artifacts", file.sealedName]);
|
|
251
|
+
if (bytes.length !== file.bytes || digest(bytes) !== file.sha256) return fail("file_changed");
|
|
252
|
+
}
|
|
253
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
254
|
+
return { ok: true, snapshotKey: input.key, requestId: input.call.requestId,
|
|
255
|
+
bindingDigest: input.call.bindingDigest, files: completed.files };
|
|
256
|
+
}
|
|
257
|
+
const work = [".collaboration", "work", input.agentId, String(input.lifecycleEpoch), input.call.requestId];
|
|
258
|
+
const outputs: SealedOutput[] = [];
|
|
259
|
+
for (const path of input.call.paths) {
|
|
260
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
261
|
+
const parts = [...work, ...path.split("/")];
|
|
262
|
+
const bytes = await this.runtime.queue(join(this.runtime.root, ...parts), async () => {
|
|
263
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
264
|
+
const bytes = core.read(parts);
|
|
265
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
266
|
+
return bytes;
|
|
267
|
+
});
|
|
268
|
+
// Queue is released before sealing/fsync/receipt IO. No HTTP, model or owner wait holds it.
|
|
269
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
270
|
+
const output = core.publish({ directory: [".collaboration", "artifacts"], name: randomUUID(), bytes, immutable: true, exact: true });
|
|
271
|
+
const checked = core.read([".collaboration", "artifacts", output.name]);
|
|
272
|
+
if (checked.length !== output.bytes || digest(checked) !== output.sha256) return fail("file_changed");
|
|
273
|
+
outputs.push({ path, sealedName: output.name, sha256: output.sha256, bytes: output.bytes,
|
|
274
|
+
mediaType: sniffImage(bytes)?.type ?? "application/octet-stream", name: path.split("/").at(-1) ?? path });
|
|
275
|
+
}
|
|
276
|
+
input.assertCurrent(); this.runtime.assertKnowledgeLock();
|
|
277
|
+
const result: SnapshotResult = { ok: true, snapshotKey: input.key, requestId: input.call.requestId,
|
|
278
|
+
bindingDigest: input.call.bindingDigest, files: outputs };
|
|
279
|
+
core.publish({ directory: receipt, name: "complete.json", bytes: Buffer.from(JSON.stringify(result)), immutable: true, exact: true });
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
}
|