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,144 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual, type KeyObject } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Authenticated call identity, not a file grant, owner credential or action authority. */
|
|
4
|
+
export interface ManagedCallIdentity {
|
|
5
|
+
readonly role: "main" | "agent";
|
|
6
|
+
readonly agentId: string | null;
|
|
7
|
+
readonly convId: string;
|
|
8
|
+
readonly convEpoch: number;
|
|
9
|
+
readonly lifecycleEpoch: number;
|
|
10
|
+
readonly sessionPath: string;
|
|
11
|
+
readonly durableSessionId: string;
|
|
12
|
+
readonly sessionIncarnation: string;
|
|
13
|
+
readonly gatewayGeneration: string;
|
|
14
|
+
readonly inputId: string;
|
|
15
|
+
readonly toolCallId: string;
|
|
16
|
+
readonly logicalCallId: string;
|
|
17
|
+
readonly activationId: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ManagedCallRequest {
|
|
21
|
+
readonly method: string;
|
|
22
|
+
/** Exact origin-form request target, including the query string. */
|
|
23
|
+
readonly route: string;
|
|
24
|
+
readonly operation: string;
|
|
25
|
+
/** Exact transmitted bytes, not a reparsed JSON object. */
|
|
26
|
+
readonly body: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ManagedCallProof {
|
|
30
|
+
readonly version: 1;
|
|
31
|
+
readonly identity: ManagedCallIdentity;
|
|
32
|
+
readonly nonce: string;
|
|
33
|
+
readonly bodySha256: string;
|
|
34
|
+
readonly mac: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ManagedCallBinding {
|
|
38
|
+
readonly identity: ManagedCallIdentity;
|
|
39
|
+
/** Provisioned only through the gateway-controlled native bind/handshake. */
|
|
40
|
+
readonly secret: KeyObject;
|
|
41
|
+
/** Atomic, synchronous claim. Retain claims until this native binding is retired. */
|
|
42
|
+
claimNonce(nonce: string): boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a CURRENT Hub/native binding and its stored logical-call mapping.
|
|
47
|
+
* The argument is an untrusted selector, never a binding to copy back.
|
|
48
|
+
* Returning null rejects retired/foreign instances or an unproven post-rotation mapping.
|
|
49
|
+
* Resolve and claim must not yield; retirement must serialize on the same owner.
|
|
50
|
+
*/
|
|
51
|
+
export type ManagedCallBindingResolver = (identity: ManagedCallIdentity) => ManagedCallBinding | null;
|
|
52
|
+
|
|
53
|
+
const verified = Symbol("VerifiedManagedCall");
|
|
54
|
+
export interface VerifiedManagedCall extends ManagedCallIdentity, ManagedCallRequest {
|
|
55
|
+
readonly [verified]: true;
|
|
56
|
+
readonly nonce: string;
|
|
57
|
+
readonly bodySha256: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ManagedCallError extends Error {
|
|
61
|
+
constructor(readonly code: "invalid_proof" | "binding_mismatch" | "invalid_mac" | "nonce_reused" | "native_context_mismatch") {
|
|
62
|
+
super(code);
|
|
63
|
+
this.name = "ManagedCallError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const IDENTITY_KEYS = [
|
|
68
|
+
"role", "agentId", "convId", "convEpoch", "lifecycleEpoch", "sessionPath", "durableSessionId",
|
|
69
|
+
"sessionIncarnation", "gatewayGeneration", "inputId", "toolCallId", "logicalCallId", "activationId",
|
|
70
|
+
] as const;
|
|
71
|
+
const HEX256 = /^[a-f0-9]{64}$/;
|
|
72
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
73
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
const text = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
75
|
+
const epoch = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
76
|
+
|
|
77
|
+
/** Parse once at the wire boundary; retain no caller-owned identity object. */
|
|
78
|
+
export function parseManagedCallIdentity(value: unknown): ManagedCallIdentity {
|
|
79
|
+
if (!record(value) || Object.keys(value).length !== IDENTITY_KEYS.length ||
|
|
80
|
+
!IDENTITY_KEYS.every(key => Object.hasOwn(value, key))) throw new ManagedCallError("invalid_proof");
|
|
81
|
+
const { role, agentId, convId, convEpoch, lifecycleEpoch, sessionPath, durableSessionId,
|
|
82
|
+
sessionIncarnation, gatewayGeneration, inputId, toolCallId, logicalCallId, activationId } = value;
|
|
83
|
+
if (!(agentId === null || text(agentId))) throw new ManagedCallError("invalid_proof");
|
|
84
|
+
switch (role) {
|
|
85
|
+
case "main": if (agentId !== null) throw new ManagedCallError("invalid_proof"); break;
|
|
86
|
+
case "agent": if (agentId === null) throw new ManagedCallError("invalid_proof"); break;
|
|
87
|
+
default: throw new ManagedCallError("invalid_proof");
|
|
88
|
+
}
|
|
89
|
+
if (!text(convId) || !epoch(convEpoch) || !epoch(lifecycleEpoch) || !text(sessionPath) ||
|
|
90
|
+
!text(durableSessionId) || !text(sessionIncarnation) || !text(gatewayGeneration) ||
|
|
91
|
+
!text(inputId) || !text(toolCallId) || !text(logicalCallId) ||
|
|
92
|
+
!(activationId === null || text(activationId))) throw new ManagedCallError("invalid_proof");
|
|
93
|
+
return Object.freeze({ role, agentId, convId, convEpoch, lifecycleEpoch, sessionPath, durableSessionId,
|
|
94
|
+
sessionIncarnation, gatewayGeneration, inputId, toolCallId, logicalCallId, activationId });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function sameManagedCallIdentity(left: ManagedCallIdentity, right: ManagedCallIdentity): boolean {
|
|
98
|
+
return IDENTITY_KEYS.every(key => left[key] === right[key]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function managedCallBodyDigest(body: Uint8Array): string {
|
|
102
|
+
return createHash("sha256").update(body).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One canonical signing implementation, shared by bridge and gateway. */
|
|
106
|
+
export function managedCallSignature(
|
|
107
|
+
binding: Pick<ManagedCallBinding, "identity" | "secret">,
|
|
108
|
+
request: ManagedCallRequest,
|
|
109
|
+
content: Pick<ManagedCallProof, "nonce" | "bodySha256">,
|
|
110
|
+
): string {
|
|
111
|
+
if (!/^[A-Z]+$/.test(request.method) || !request.route.startsWith("/") || request.route.startsWith("//") ||
|
|
112
|
+
/[\r\n#]/.test(request.route) || !text(request.operation) ||
|
|
113
|
+
!HEX256.test(content.nonce) || !HEX256.test(content.bodySha256)) throw new ManagedCallError("invalid_proof");
|
|
114
|
+
if (binding.secret.type !== "secret" || binding.secret.symmetricKeySize !== 32) throw new ManagedCallError("invalid_proof");
|
|
115
|
+
const message = JSON.stringify(["zigmini.managed-call", 1, IDENTITY_KEYS.map(key => binding.identity[key]),
|
|
116
|
+
request.method, request.route, request.operation, content.bodySha256, content.nonce]);
|
|
117
|
+
return createHmac("sha256", binding.secret).update(message).digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Synchronous authentication and one-use claim. The result is a snapshot only:
|
|
122
|
+
* consumers still recheck binding/revisions at their final commit/content release.
|
|
123
|
+
*/
|
|
124
|
+
export function verifyManagedCall(
|
|
125
|
+
request: ManagedCallRequest & { readonly proof: unknown },
|
|
126
|
+
bindingResolver: ManagedCallBindingResolver,
|
|
127
|
+
): VerifiedManagedCall {
|
|
128
|
+
const proof = request.proof;
|
|
129
|
+
if (!record(proof) || Object.keys(proof).length !== 5 || proof.version !== 1 ||
|
|
130
|
+
typeof proof.nonce !== "string" || !HEX256.test(proof.nonce) ||
|
|
131
|
+
typeof proof.bodySha256 !== "string" || !HEX256.test(proof.bodySha256) ||
|
|
132
|
+
typeof proof.mac !== "string" || !HEX256.test(proof.mac)) throw new ManagedCallError("invalid_proof");
|
|
133
|
+
const identity = parseManagedCallIdentity(proof.identity);
|
|
134
|
+
const binding = bindingResolver(identity);
|
|
135
|
+
if (binding === null || !sameManagedCallIdentity(identity, binding.identity)) throw new ManagedCallError("binding_mismatch");
|
|
136
|
+
const bodySha256 = managedCallBodyDigest(request.body);
|
|
137
|
+
if (bodySha256 !== proof.bodySha256) throw new ManagedCallError("invalid_mac");
|
|
138
|
+
const content = { nonce: proof.nonce, bodySha256 };
|
|
139
|
+
const mac = managedCallSignature(binding, request, content);
|
|
140
|
+
if (!timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(proof.mac, "hex"))) throw new ManagedCallError("invalid_mac");
|
|
141
|
+
if (!binding.claimNonce(proof.nonce)) throw new ManagedCallError("nonce_reused");
|
|
142
|
+
return Object.freeze({ ...identity, method: request.method, route: request.route, operation: request.operation,
|
|
143
|
+
body: Uint8Array.from(request.body), nonce: proof.nonce, bodySha256, [verified]: true as const });
|
|
144
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
AgentAccessView, AuthorizationOutcome, GrantReconcile, RegisteredToolMeta, RestrictionAdapter, ToolAvailability,
|
|
4
|
+
ToolDescriptor, ToolEffect, ToolGrant, ToolKind, ToolSourceKind,
|
|
5
|
+
} from "./tool-permission-types.ts";
|
|
6
|
+
|
|
7
|
+
interface CatalogEntry {
|
|
8
|
+
readonly exposedName: string;
|
|
9
|
+
readonly sourceKind: ToolSourceKind;
|
|
10
|
+
readonly ownerId: string;
|
|
11
|
+
readonly originalName: string;
|
|
12
|
+
readonly kind: ToolKind;
|
|
13
|
+
readonly effect: ToolEffect;
|
|
14
|
+
readonly operations: readonly string[];
|
|
15
|
+
readonly restrictionAdapter: RestrictionAdapter;
|
|
16
|
+
readonly availability: ToolAvailability;
|
|
17
|
+
readonly bootstrap: boolean;
|
|
18
|
+
readonly grantable: boolean;
|
|
19
|
+
readonly roleOnly: "work" | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Row = [names: readonly string[], kind: ToolKind, effect: ToolEffect, operations: readonly string[], adapter: RestrictionAdapter];
|
|
23
|
+
|
|
24
|
+
const ENGINE_ROWS: readonly Row[] = [
|
|
25
|
+
[["read", "grep", "find", "glob", "ls"], "file_read", "read", ["read", "list"], "path_scope"],
|
|
26
|
+
[["edit", "write", "apply_patch"], "file_write", "local_write", ["write"], "path_scope"],
|
|
27
|
+
[["bash", "powershell", "eval", "monitor"], "exec", "execution", ["execute"], "restricted_exec"],
|
|
28
|
+
[["bash_output", "bash_input", "bash_resize", "kill_bash"], "exec_control", "control", ["control_owned"], "owned_exec_handle"],
|
|
29
|
+
[["web_search", "$web_search"], "web", "read", ["search"], "web_origin"],
|
|
30
|
+
[["webfetch"], "web", "read", ["fetch"], "web_origin"],
|
|
31
|
+
[["generate_image"], "media", "external_write", ["generate"], "media_io"],
|
|
32
|
+
[["read_video", "look_at"], "media", "read", ["inspect"], "media_io"],
|
|
33
|
+
[["show_html_page"], "display", "read", ["display"], "display_surface"],
|
|
34
|
+
[["lsp_diagnostics", "lsp_goto_definition", "lsp_find_references", "lsp_symbols", "lsp_prepare_rename"], "lsp", "read", ["read"], "path_scope"],
|
|
35
|
+
[["lsp_rename"], "lsp", "local_write", ["write"], "path_scope"],
|
|
36
|
+
[["computer", "omo_browser_bridge", "omo_desktop_toolkit"], "browser", "control", ["observe", "act"], "browser_target"],
|
|
37
|
+
[["task", "task_send", "task_cancel", "task_output", "workflow", "workpool", "team_create", "team_delete", "task_create", "task_get", "task_list", "task_update"],
|
|
38
|
+
"subtask", "control", ["control_owned"], "owned_child"],
|
|
39
|
+
[["thread_create", "thread_list", "thread_read", "thread_send", "thread_interrupt", "thread_handoff", "thread_rename", "thread_set_model",
|
|
40
|
+
"thread_set_reasoning", "thread_bind", "thread_unbind", "thread_rebind", "thread_bindings", "thread_report", "thread_outbox", "thread_outbox_ack", "thread_answer"],
|
|
41
|
+
"session", "control", ["control_owned"], "owned_session"],
|
|
42
|
+
[["todo", "create_goal", "update_goal", "get_goal", "schedule_wakeup", "schedule_prompt"], "self_state", "control", ["self"], "self_state"],
|
|
43
|
+
[["onboarding_session_history", "onboarding_understand"], "memory", "read", ["read_self"], "self_identity"],
|
|
44
|
+
[["onboarding_memory_apply", "memory"], "memory", "local_write", ["write_self"], "self_identity"],
|
|
45
|
+
[["list_relocation_targets", "relocate_session"], "relocation", "control", ["relocate"], "relocation_target"],
|
|
46
|
+
[["request_user_input"], "communication", "control", ["ask"], "metadata_only"],
|
|
47
|
+
[["tool_search"], "discovery", "read", ["search_metadata"], "metadata_only"],
|
|
48
|
+
[["mcp_list_resources", "mcp_read_resource"], "mcp_resource", "read", ["read"], "mcp_resource"],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const entry = (exposedName: string, sourceKind: ToolSourceKind, ownerId: string, originalName: string, kind: ToolKind, effect: ToolEffect,
|
|
52
|
+
operations: readonly string[], restrictionAdapter: RestrictionAdapter, availability: ToolAvailability,
|
|
53
|
+
extra: Partial<Pick<CatalogEntry, "bootstrap" | "grantable" | "roleOnly">> = {}): CatalogEntry =>
|
|
54
|
+
({ exposedName, sourceKind, ownerId, originalName, kind, effect, operations, restrictionAdapter, availability, bootstrap: false, grantable: true, roleOnly: null, ...extra });
|
|
55
|
+
|
|
56
|
+
const mcpEntry = (exposedName: string, server: string, tool: string, kind: ToolKind, effect: ToolEffect, operations: readonly string[], adapter: RestrictionAdapter) =>
|
|
57
|
+
entry(exposedName, "mcp", server, tool, kind, effect, operations, adapter, "mcp_dynamic");
|
|
58
|
+
|
|
59
|
+
const gateway = (name: string, kind: ToolKind, effect: ToolEffect, operations: readonly string[], adapter: RestrictionAdapter, availability: ToolAvailability,
|
|
60
|
+
extra: Partial<Pick<CatalogEntry, "bootstrap" | "grantable" | "roleOnly">> = {}) =>
|
|
61
|
+
entry(name, "gateway", "gateway", name, kind, effect, operations, adapter, availability, extra);
|
|
62
|
+
|
|
63
|
+
/** Every classified tool. Anything not found here by (sourceKind, ownerId, originalName) is opaque and default-deny. */
|
|
64
|
+
export const CATALOG: readonly CatalogEntry[] = [
|
|
65
|
+
...ENGINE_ROWS.flatMap(([names, kind, effect, operations, adapter]) =>
|
|
66
|
+
names.map((name) => entry(name, "builtin", "engine", name, kind, effect, operations, adapter, "registered"))),
|
|
67
|
+
entry("ask_user_question", "builtin", "engine", "ask_user_question", "communication", "control", ["ask"], "metadata_only", "registered", { bootstrap: true }),
|
|
68
|
+
entry("web_search_exa", "builtin", "engine", "web_search_exa", "web", "read", ["search"], "not_grantable", "internal_provider", { grantable: false }),
|
|
69
|
+
entry("mcp", "builtin", "engine", "mcp", "mcp_proxy", "control", ["invoke"], "mcp_proxy", "registered", { grantable: false }),
|
|
70
|
+
mcpEntry("mcp_context7_query-docs", "context7", "query-docs", "mcp_docs", "read", ["query"], "web_origin"),
|
|
71
|
+
mcpEntry("mcp_context7_resolve-library-id", "context7", "resolve-library-id", "mcp_docs", "read", ["query"], "web_origin"),
|
|
72
|
+
mcpEntry("mcp_grep_app_searchGitHub", "grep_app", "searchGitHub", "mcp_docs", "read", ["query"], "web_origin"),
|
|
73
|
+
mcpEntry("mcp__ast_grep_scan", "ast_grep", "scan", "mcp_ast", "read", ["read"], "path_scope"),
|
|
74
|
+
mcpEntry("mcp__ast_grep_search", "ast_grep", "search", "mcp_ast", "read", ["read"], "path_scope"),
|
|
75
|
+
mcpEntry("mcp__ast_grep_rewrite", "ast_grep", "rewrite", "mcp_ast", "local_write", ["write"], "path_scope"),
|
|
76
|
+
gateway("zgw_work_report", "work_role", "control", ["report"], "role_only", "gateway_work", { roleOnly: "work" }),
|
|
77
|
+
gateway("zgw_agent_history_search", "agent_history", "read", ["search"], "own_history", "gateway_agent"),
|
|
78
|
+
gateway("zgw_attachment_read", "attachment", "read", ["read"], "attachment_selection", "gateway_agent"),
|
|
79
|
+
// 294 C7: the narrow Aside browser tool of a registered agent (host/omo-bridge/agent-control.ts registers it).
|
|
80
|
+
gateway("zgw_aside", "browser", "control", ["read", "view", "open_owned", "navigate", "click", "fill", "close"], "browser_target", "gateway_agent"),
|
|
81
|
+
gateway("zgw_tool_catalog", "bootstrap", "control", ["list_metadata"], "metadata_only", "planned", { bootstrap: true }),
|
|
82
|
+
gateway("zgw_tool_request", "bootstrap", "control", ["request"], "metadata_only", "planned", { bootstrap: true }),
|
|
83
|
+
gateway("zgw_tool_status", "bootstrap", "control", ["status_self"], "metadata_only", "planned", { bootstrap: true }),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const identity = (sourceKind: string, ownerId: string, originalName: string) => JSON.stringify([sourceKind, ownerId, originalName]);
|
|
87
|
+
const BY_IDENTITY = new Map(CATALOG.map((row) => [identity(row.sourceKind, row.ownerId, row.originalName), row]));
|
|
88
|
+
|
|
89
|
+
function canonical(value: unknown): unknown {
|
|
90
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
91
|
+
if (value && typeof value === "object")
|
|
92
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical((value as Record<string, unknown>)[key])]));
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** sha256 over source, owner, original name and parameter schema. The exposed name is display only and excluded. */
|
|
97
|
+
export function registrationDigest(meta: RegisteredToolMeta): string {
|
|
98
|
+
const body = JSON.stringify(canonical({ sourceKind: meta.sourceKind, ownerId: meta.ownerId, originalName: meta.originalName, parameters: meta.parameters ?? null }));
|
|
99
|
+
return createHash("sha256").update(body).digest("hex");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function describeTool(meta: RegisteredToolMeta): ToolDescriptor {
|
|
103
|
+
const digest = registrationDigest(meta);
|
|
104
|
+
const key = { sourceKind: meta.sourceKind, ownerId: meta.ownerId, originalName: meta.originalName, registrationDigest: digest };
|
|
105
|
+
const known = BY_IDENTITY.get(identity(meta.sourceKind, meta.ownerId, meta.originalName));
|
|
106
|
+
if (!known) {
|
|
107
|
+
return { key, exposedName: meta.name, kind: "unclassified", effect: "external_write", operations: ["invoke_exact"], scopeSchema: "exact_args",
|
|
108
|
+
availability: "unknown", restrictionAdapter: "exact_args", registrationDigest: digest, opaque: true, bootstrap: false, grantable: true, roleOnly: null };
|
|
109
|
+
}
|
|
110
|
+
return { key, exposedName: meta.name, kind: known.kind, effect: known.effect, operations: known.operations, scopeSchema: known.restrictionAdapter,
|
|
111
|
+
availability: known.availability, restrictionAdapter: known.restrictionAdapter, registrationDigest: digest, opaque: false,
|
|
112
|
+
bootstrap: known.bootstrap, grantable: known.grantable, roleOnly: known.roleOnly };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sameTool = (grant: ToolGrant, descriptor: ToolDescriptor) =>
|
|
116
|
+
grant.toolKey.sourceKind === descriptor.key.sourceKind && grant.toolKey.ownerId === descriptor.key.ownerId
|
|
117
|
+
&& grant.toolKey.originalName === descriptor.key.originalName;
|
|
118
|
+
|
|
119
|
+
/** A grant stays active only for the same registration; a changed schema or source suspends it as tool_changed. */
|
|
120
|
+
export function reconcileGrant(grant: ToolGrant, descriptor: ToolDescriptor): GrantReconcile {
|
|
121
|
+
if (!sameTool(grant, descriptor)) return { state: "unrelated" };
|
|
122
|
+
if (grant.toolKey.registrationDigest !== descriptor.registrationDigest) return { state: "suspended", reason: "tool_changed" };
|
|
123
|
+
return { state: "active" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** zgw_aside input operation -> catalog operation (C7). A grant names catalog operations. */
|
|
127
|
+
export const ASIDE_OPERATIONS: Readonly<Record<string, string>> = {
|
|
128
|
+
read: "read", screenshot: "view", open_owned: "open_owned", navigate_owned: "navigate", click: "click", fill: "fill", close_owned: "close",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The request identity of a call the engine held: the catalog source and owner by exposed name (an unlisted tool stays
|
|
133
|
+
* opaque under its own source) and the catalog operation the call needs. MCP names are never resolved by exposed name.
|
|
134
|
+
*/
|
|
135
|
+
export function heldToolRequest(tool: { readonly name: string; readonly parameters: unknown; readonly sourceKind?: ToolSourceKind }, args: unknown):
|
|
136
|
+
{ readonly toolKey: ToolDescriptor["key"]; readonly operation: string } {
|
|
137
|
+
const known = CATALOG.find((row) => row.exposedName === tool.name && row.sourceKind !== "mcp");
|
|
138
|
+
const descriptor = describeTool({ name: tool.name, sourceKind: known?.sourceKind ?? tool.sourceKind ?? "extension", ownerId: known?.ownerId ?? "unknown",
|
|
139
|
+
originalName: tool.name, parameters: tool.parameters });
|
|
140
|
+
const asked = tool.name === "zgw_aside" && args !== null && typeof args === "object" ? (args as { operation?: unknown }).operation : undefined;
|
|
141
|
+
const operation = typeof asked === "string" && Object.hasOwn(ASIDE_OPERATIONS, asked) ? ASIDE_OPERATIONS[asked]! : descriptor.operations[0]!;
|
|
142
|
+
return { toolKey: descriptor.key, operation };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface AuthorizeInput {
|
|
146
|
+
readonly descriptor: ToolDescriptor;
|
|
147
|
+
readonly grant: ToolGrant | null;
|
|
148
|
+
readonly access: AgentAccessView;
|
|
149
|
+
readonly operation: string;
|
|
150
|
+
readonly role: "agent" | "work";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function authorizeTool(input: AuthorizeInput): AuthorizationOutcome {
|
|
154
|
+
const { descriptor, grant, access, operation } = input;
|
|
155
|
+
if (access.accessState !== "open") return { decision: "deny", reason: "agent_access_blocked" };
|
|
156
|
+
if (!descriptor.grantable) return { decision: "deny", reason: "not_grantable" };
|
|
157
|
+
if (descriptor.roleOnly !== null && descriptor.roleOnly !== input.role) return { decision: "deny", reason: "role_only" };
|
|
158
|
+
if (!descriptor.operations.includes(operation)) return { decision: "deny", reason: "unknown_operation" };
|
|
159
|
+
if (descriptor.bootstrap) return { decision: "allow" };
|
|
160
|
+
if (grant === null) return { decision: "deny", reason: "no_grant" };
|
|
161
|
+
if (grant.agentId !== access.agentId) return { decision: "deny", reason: "grant_agent_mismatch" };
|
|
162
|
+
if (grant.incarnation !== access.lifecycleEpoch) return { decision: "deny", reason: "lifecycle_epoch_mismatch" };
|
|
163
|
+
if (grant.authorityEpoch !== access.authorityEpoch) return { decision: "deny", reason: "authority_epoch_mismatch" };
|
|
164
|
+
if (grant.state !== "active") return { decision: "deny", reason: "grant_not_active" };
|
|
165
|
+
const reconcile = reconcileGrant(grant, descriptor);
|
|
166
|
+
if (reconcile.state === "unrelated") return { decision: "deny", reason: "grant_tool_mismatch" };
|
|
167
|
+
if (reconcile.state === "suspended") return { decision: "hold", reason: "tool_changed" };
|
|
168
|
+
if (!grant.operations.includes(operation)) return { decision: "deny", reason: "operation_not_granted" };
|
|
169
|
+
return { decision: "allow" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CallInput extends Omit<AuthorizeInput, "descriptor"> {
|
|
173
|
+
readonly tool: RegisteredToolMeta;
|
|
174
|
+
/** Resolved inner tool of a proxy/nested call; null when it could not be resolved. */
|
|
175
|
+
readonly target: RegisteredToolMeta | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A proxy exposure never grants anything itself: the inner tool is checked as if called directly. */
|
|
179
|
+
export function authorizeCall(input: CallInput): AuthorizationOutcome {
|
|
180
|
+
const outer = describeTool(input.tool);
|
|
181
|
+
if (outer.restrictionAdapter !== "mcp_proxy") return authorizeTool({ ...input, descriptor: outer });
|
|
182
|
+
if (input.target === null) return { decision: "deny", reason: "proxy_target_unresolved" };
|
|
183
|
+
return authorizeTool({ ...input, descriptor: describeTool(input.target) });
|
|
184
|
+
}
|