zigmini 1.0.0-beta.3 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +27 -2
- package/README.md +27 -2
- 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 +114 -10
- package/docs/manual.md +117 -11
- 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-gateway-port.manifest.json +28 -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 +228 -30
- package/src/message-references.ts +14 -5
- package/src/profile-prompt.ts +13 -1
- 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/work-watch.ts +55 -7
- package/src/work.ts +192 -3
- package/web/DESIGN.md +33 -0
- 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 +11 -6
- 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
|
@@ -1,41 +1,61 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { registerAgentCollaboration, COLLABORATION_TOOLS, CollaborationExtensionError } from "./agent-collaboration.ts";
|
|
6
|
+
import { CollaborationSnapshots, type NativeFileSdk } from "./agent-collaboration-files.ts";
|
|
4
7
|
import { INSTALL_RULES_FILE, INSTALL_RULES_HEADING, packageRulesDrop, PACKAGE_RULES_FILE, PACKAGE_RULES_HEADING, readRulesFile } from "../../src/agent-rules.ts";
|
|
5
8
|
import { applyRequestRules, loadRequestRules, type ProviderRequestEvent } from "../../src/agent-request-rules.ts";
|
|
6
9
|
import { loadEngineSdk } from "../../src/engine/index.ts";
|
|
7
10
|
import { validateAgentFields } from "../../src/agent/definition.ts";
|
|
8
11
|
import { runtimeHome, runtimePaths } from "../../src/runtime/paths.ts";
|
|
9
|
-
import {
|
|
12
|
+
import { ATTACHMENT_READ_TOOL, installAttachmentRead, type AttachmentExtension } from "./attachment-read.ts";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import { agentToolGatewayClient, installAgentToolBinding, type AgentToolBindExtension, type AgentToolRoute } from "./agent-tool-permissions.ts";
|
|
15
|
+
import { AsideAdapter, asideBin } from "./agent-aside.ts";
|
|
16
|
+
import { ActionTickets, AsideTargetRegistry, actionDigest } from "../../src/agent/aside-targets.ts";
|
|
17
|
+
import { agentToolActionId, agentToolOperationId } from "../../src/agent/tool-delivery.ts";
|
|
18
|
+
import { heldToolRequest } from "../../src/agent/tool-catalog.ts";
|
|
19
|
+
import type { ToolSourceKind } from "../../src/agent/tool-permission-types.ts";
|
|
20
|
+
import { ASIDE_DESCRIPTION, ASIDE_PARAMETERS, ASIDE_TOOL, asideAccount, asideCallDecision, asideGrantedOperations, asidePermits, heldAsideScope, carriedAsideTarget, heldOperation, withCarriedTarget } from "./agent-aside-tool.ts";
|
|
21
|
+
import { canonicalJson } from "../../src/agent/tool-permissions.ts";
|
|
22
|
+
import type { SessionAsideTarget } from "../../src/agent/aside-target-routes.ts";
|
|
23
|
+
import { agentNamePrompt, agentNicknamePrompt, readAgentPromptName, readPromptProfile, type PromptProfile } from "../../src/profile-prompt.ts";
|
|
10
24
|
|
|
11
|
-
type
|
|
25
|
+
type Entry = { readonly type: string; readonly customType?: string; readonly data?: Record<string, unknown> };
|
|
26
|
+
type Context = { readonly cwd: string; readonly sessionManager: { getSessionId(): string; getEntries?(): readonly Entry[] }; readonly model?: unknown; readonly thinkingLevel?: string; readonly ui: {
|
|
27
|
+
notify(message: string, level: "warning"): void;
|
|
28
|
+
select(title: string, options: string[], opts?: { toolCallId?: string }): Promise<string | undefined>;
|
|
29
|
+
} };
|
|
12
30
|
type ToolCall = { readonly toolName: string; readonly toolCallId: string; readonly input: Record<string, unknown> };
|
|
13
31
|
type Block = { readonly block: true; readonly reason: string };
|
|
14
32
|
type Skill = { readonly name: string; readonly filePath: string };
|
|
15
33
|
type Start = { readonly systemPrompt: string; readonly systemPromptOptions: { readonly skills?: readonly Skill[] } };
|
|
16
34
|
type Events = {
|
|
17
35
|
session_start: unknown; session_shutdown: unknown; before_agent_start: Start;
|
|
18
|
-
before_provider_request: ProviderRequestEvent; tool_call: ToolCall; tool_result: { readonly toolCallId: string };
|
|
19
|
-
input: { readonly text: string }; agent_end: unknown; session_abort: unknown;
|
|
36
|
+
before_provider_request: ProviderRequestEvent; tool_call: ToolCall; tool_result: { readonly toolCallId: string; readonly isError: boolean };
|
|
37
|
+
input: { readonly text: string; readonly inputId?: string }; agent_end: unknown; agent_settled: unknown; session_abort: unknown;
|
|
38
|
+
tool_execution_end: { readonly toolCallId: string; readonly toolName: string; readonly result: unknown; readonly isError: boolean };
|
|
20
39
|
};
|
|
21
40
|
interface ExtensionApi {
|
|
22
41
|
readonly sessionKind: string;
|
|
23
42
|
readonly sessionContext: Readonly<Record<string, string>>;
|
|
24
|
-
readonly rpc: { emit(name: string, data: unknown): void; handle(name: string, handler: () => unknown): void };
|
|
43
|
+
readonly rpc: { emit(name: string, data: unknown): void; handle(name: string, handler: (data: unknown) => unknown): void };
|
|
25
44
|
on<K extends keyof Events>(event: K, handler: (event: Events[K], ctx: Context) => unknown, options?: { previewSafe: boolean }): void;
|
|
26
45
|
setActiveTools(names: string[]): void;
|
|
27
46
|
getCommands(): readonly { readonly name: string; readonly source: string }[];
|
|
28
47
|
appendEntry(name: string, data: unknown): void;
|
|
29
48
|
registerTool(tool: HistoryTool): void;
|
|
49
|
+
getAllTools?(): readonly { readonly name: string; readonly parameters?: unknown; readonly sourceInfo?: { readonly source?: string } }[];
|
|
30
50
|
registerFilesystemPolicy(policy: { check(request: { operation: string; canonicalPath: string }): { allow: true } | { allow: false; reason: string } }): void;
|
|
31
51
|
}
|
|
32
52
|
type HistoryTool = {
|
|
33
53
|
name: string; label: string; description: string; parameters: unknown;
|
|
34
|
-
execute(id: string, params: Record<string, unknown>, signal: AbortSignal | undefined): Promise<unknown>;
|
|
54
|
+
execute(id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, update?: unknown, ctx?: unknown): Promise<unknown>;
|
|
35
55
|
};
|
|
36
56
|
type Edit = { readonly oldText: string; readonly newText: string };
|
|
37
57
|
/** The runtime supplies this public SDK. Tests use the same native editor, not a replacement algorithm. */
|
|
38
|
-
export interface AgentSdk {
|
|
58
|
+
export interface AgentSdk extends NativeFileSdk {
|
|
39
59
|
createEditTool(cwd: string, options: { operations: {
|
|
40
60
|
readFile(path: string): Promise<Buffer>; writeFile(path: string, content: string): Promise<void>; access(path: string): Promise<void>;
|
|
41
61
|
} }): { execute(id: string, input: { path: string; edits: Edit[] }): Promise<unknown> };
|
|
@@ -44,9 +64,38 @@ const BASE_TOOLS = ["read", "write", "edit", "ask_user_question", "zgw_agent_his
|
|
|
44
64
|
export const MEMORY_CAP_MESSAGE = "MEMORY.md를 요약해 줄인 뒤 다시 쓰세요.";
|
|
45
65
|
const DAILY_RULES = "Record each day's facts, results and decisions chronologically in notes/daily/YYYY-MM-DD.md (KST). " +
|
|
46
66
|
"Keep only long-lived preferences, established facts and ongoing goals summarized in MEMORY.md; do not copy daily logs into it.";
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
// The session cwd is knowledge/, so these paths are relative to it.
|
|
68
|
+
export const RESEARCH_RULES = "When you research on the web, write the results to notes/companies/<company>.md " +
|
|
69
|
+
"(other topics notes/research/<topic>.md) and put each source URL on the line of the fact it supports.";
|
|
70
|
+
export const WRITE_SCOPE_REASON = "agent_write_outside_outputs: Write result files under outputs/ and notes under notes/.";
|
|
71
|
+
/**
|
|
72
|
+
* C1: daily-note and research write instructions (and the daily reminder) ask for writes. A managed agent (the Hub pushed a
|
|
73
|
+
* policy) gets them only while write or edit is granted; a legacy agent keeps them.
|
|
74
|
+
*/
|
|
75
|
+
export function writeInstructionsAllowed(granted: { readonly revision: number; readonly names: readonly string[] }): boolean {
|
|
76
|
+
return granted.revision < 1 || granted.names.includes("write") || granted.names.includes("edit");
|
|
77
|
+
}
|
|
78
|
+
export const OUTSIDE_KNOWLEDGE_REASON = "agent_path_outside_knowledge";
|
|
79
|
+
/**
|
|
80
|
+
* 294 C6: a managed agent session opens full-access (the agent authority is its single gate), so the native preset no longer
|
|
81
|
+
* asks external_directory before a file tool leaves knowledge/. The adapter asks the owner in the same native form instead,
|
|
82
|
+
* and the call stays refused whatever the answer: no file outside knowledge/ is ever read or written, and nothing reaches the
|
|
83
|
+
* model while the prompt is open (293 J01). Only "Deny" is offered because no answer can let the call run.
|
|
84
|
+
*/
|
|
85
|
+
export function outsideKnowledgePrompt(lexical: string, canonical: string): string {
|
|
86
|
+
const patterns = [...new Set([lexical, canonical])].map((path) => ` - ${path}`).join("\n");
|
|
87
|
+
return `Permission required: external_directory\n\nDirectory: ${dirname(lexical)}\n\nPatterns:\n${patterns}`;
|
|
88
|
+
}
|
|
89
|
+
export const WRITE_DOTDOT_REASON = "agent_path_dotdot: A write path must not contain a .. segment; name the file under outputs/ or notes/ directly.";
|
|
90
|
+
/** The KST calendar date (YYYY-MM-DD) of a time; daily notes are named by it. */
|
|
91
|
+
export function kstDate(ms: number): string { return new Date(ms + 9 * 60 * 60 * 1000).toISOString().slice(0, 10); }
|
|
92
|
+
export function dailyReminder(date: string): string {
|
|
93
|
+
return `Reminder: today's notes/daily/${date}.md has no entry yet. Add today's facts, results and decisions before you finish this turn.`;
|
|
94
|
+
}
|
|
95
|
+
function dailyPath(root: string, date: string): string { return join(root, "notes", "daily", `${date}.md`); }
|
|
49
96
|
const HISTORY_LIMIT = 50;
|
|
97
|
+
/** Each listed area keeps this many files and this many folder levels; the model reads contents itself. */
|
|
98
|
+
const LIST_LIMIT = 200, LIST_DEPTH = 3;
|
|
50
99
|
const blocked = (reason: string): Block => ({ block: true, reason });
|
|
51
100
|
function record(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
52
101
|
function missing(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "ENOENT"; }
|
|
@@ -54,6 +103,56 @@ function inside(root: string, path: string): boolean {
|
|
|
54
103
|
const part = relative(root, path);
|
|
55
104
|
return part === "" || (!isAbsolute(part) && part !== ".." && !part.startsWith(`..${sep}`));
|
|
56
105
|
}
|
|
106
|
+
function size(bytes: number): string {
|
|
107
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
108
|
+
return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A path with a control character, a line separator (U+0085, U+2028, U+2029) or an angle bracket is shown as a JSON string
|
|
112
|
+
* with those characters escaped. Folder names join into one path, so a tag can be made across "/"; escaping the whole
|
|
113
|
+
* path means no listed line can add a line or hold the block's tags, and JSON.parse gives back the real path.
|
|
114
|
+
*/
|
|
115
|
+
function shown(path: string): string {
|
|
116
|
+
if (!/[\u0000-\u001f\u007f-\u009f\u2028\u2029<>]/.test(path)) return path;
|
|
117
|
+
return JSON.stringify(path).replace(/[\u007f-\u009f\u2028\u2029<>]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
118
|
+
}
|
|
119
|
+
/** Regular files of one area, sorted, without hidden names or links. The area itself must be a real folder. */
|
|
120
|
+
function listArea(root: string, area: string): { lines: string[]; more: number } {
|
|
121
|
+
const top = join(root, area);
|
|
122
|
+
try { if (!lstatSync(top).isDirectory()) throw new Error("knowledge_listing_failed"); } catch (error) {
|
|
123
|
+
if (missing(error)) return { lines: [], more: 0 };
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
let more = 0;
|
|
128
|
+
const walk = (dir: string, parts: readonly string[]) => {
|
|
129
|
+
for (const name of readdirSync(dir).sort()) {
|
|
130
|
+
if (name.startsWith(".")) continue;
|
|
131
|
+
const entry = lstatSync(join(dir, name));
|
|
132
|
+
if (entry.isSymbolicLink()) continue;
|
|
133
|
+
if (entry.isDirectory()) { if (parts.length + 1 < LIST_DEPTH) walk(join(dir, name), [...parts, name]); }
|
|
134
|
+
else if (!entry.isFile()) continue;
|
|
135
|
+
else if (lines.length < LIST_LIMIT) lines.push(`- ${shown([area, ...parts, name].join("/"))} (${size(entry.size)})`);
|
|
136
|
+
else more++;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(top, []);
|
|
140
|
+
return { lines, more };
|
|
141
|
+
}
|
|
142
|
+
/** Read every turn, so an upload reaches an open session at its next turn. */
|
|
143
|
+
function filesBlock(root: string, outputRoot: string | null = root): string {
|
|
144
|
+
const area = (name: string) => {
|
|
145
|
+
const { lines, more } = name === "outputs" && outputRoot === null ? { lines: [], more: 0 } : listArea(name === "outputs" ? outputRoot ?? root : root, name);
|
|
146
|
+
return [...(lines.length > 0 ? lines : ["(none)"]), ...(more > 0 ? [`- and ${more} more`] : [])];
|
|
147
|
+
};
|
|
148
|
+
return ["<agent-files>", "Files the user uploaded (source/, read-only):", ...area("source"),
|
|
149
|
+
"Outputs (outputs/, write result files here):", ...area("outputs"),
|
|
150
|
+
"When the user mentions uploaded material, read these source/ files with the read tool.", "</agent-files>"].join("\n");
|
|
151
|
+
}
|
|
152
|
+
/** The agent writes results to outputs/, notes to notes/ and its memory to MEMORY.md; nothing else. */
|
|
153
|
+
function writable(root: string, path: string): boolean {
|
|
154
|
+
return path === join(root, "MEMORY.md") || inside(join(root, "notes"), path) || inside(join(root, "outputs"), path);
|
|
155
|
+
}
|
|
57
156
|
/** Resolve existing links, including a new target's nearest existing parent. Dangling links fail closed. */
|
|
58
157
|
export function agentPath(path: string): string {
|
|
59
158
|
try { lstatSync(path); } catch (error) {
|
|
@@ -93,14 +192,38 @@ function claimLock(root: string, sessionId: string, release = false): boolean {
|
|
|
93
192
|
} finally { rmdirSync(claim); }
|
|
94
193
|
}
|
|
95
194
|
|
|
96
|
-
|
|
195
|
+
// allow: SIZE_OK - existing single-session lifecycle closure; this bounded integration retains its shared lock, memory and input state.
|
|
196
|
+
/**
|
|
197
|
+
* Q-07: the collaboration binding a 294 card is signed with, or null for an owner card. A card asked inside a peer request
|
|
198
|
+
* execution names that activation and its request; a peer without its consumed binding is refused, never sent as an owner
|
|
199
|
+
* card. The reply run of a request the owner started (an activation with no request to execute, as the request tool's
|
|
200
|
+
* `request_forbidden` check reads it) continues the owner's own work, so its card is the owner card.
|
|
201
|
+
*/
|
|
202
|
+
export function collaborationToolCard(control: { isPeer(): boolean; observeClientInput(): void;
|
|
203
|
+
capture(): { readonly identity: { readonly activationId: string | null }; readonly requestId: string | null } } | undefined): { activationId: string; requestId: string } | null {
|
|
204
|
+
if (!control?.isPeer()) return null;
|
|
205
|
+
// An engine-held call never reached the tool_call hook, so its consumed input is proven here.
|
|
206
|
+
control.observeClientInput();
|
|
207
|
+
const { identity, requestId } = control.capture();
|
|
208
|
+
if (identity.activationId !== null && requestId === null) return null;
|
|
209
|
+
if (identity.activationId === null || requestId === null) throw new CollaborationExtensionError("binding_missing");
|
|
210
|
+
return { activationId: identity.activationId, requestId };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<AgentSdk> = () => loadEngineSdk<AgentSdk>(), now: () => number = Date.now): void {
|
|
214
|
+
const attachmentRead = installAttachmentRead(pi as unknown as AttachmentExtension);
|
|
97
215
|
const id = pi.sessionContext.zgw_agent;
|
|
98
216
|
if (pi.sessionContext.zgw_role !== "agent" || pi.sessionKind !== "interactive" || !id) return;
|
|
217
|
+
const collaboration = registerAgentCollaboration(pi);
|
|
218
|
+
let sdkPromise: Promise<AgentSdk> | undefined;
|
|
219
|
+
const sdkForSession = () => sdkPromise ??= loadSdk();
|
|
220
|
+
let fileBoundary: CollaborationSnapshots | undefined;
|
|
99
221
|
pi.registerTool({
|
|
100
222
|
name: "zgw_agent_history_search", label: "Agent history search",
|
|
101
223
|
description: "Search this agent's own past conversation messages for a text. Returns the newest matches first (at most 50). Other conversations are never searched.",
|
|
102
224
|
parameters: { type: "object", properties: { query: { type: "string", minLength: 1, maxLength: 200 }, limit: { type: "integer", minimum: 1, maximum: HISTORY_LIMIT } }, required: ["query"], additionalProperties: false },
|
|
103
225
|
async execute(_toolCallId, params, signal) {
|
|
226
|
+
collaboration?.assertGeneralTool();
|
|
104
227
|
if (typeof params.query !== "string" || params.query === "") throw new Error("agent_history_query_required");
|
|
105
228
|
const limit = typeof params.limit === "number" && Number.isInteger(params.limit) ? Math.min(Math.max(params.limit, 1), HISTORY_LIMIT) : 20;
|
|
106
229
|
const token = readFileSync(runtimePaths().omoTokenFile, "utf8").trim();
|
|
@@ -117,15 +240,189 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
117
240
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
118
241
|
},
|
|
119
242
|
});
|
|
120
|
-
let root = "", sessionId = "",
|
|
243
|
+
let root = "", sessionId = "", ready = false, agentName = "";
|
|
244
|
+
// The role prompt sections of this session; the write-instruction sections are filtered per turn (C1).
|
|
245
|
+
let promptParts: string[] = [];
|
|
121
246
|
let maxBytes = 0;
|
|
122
247
|
let tools: string[] = [], skills: string[] = [];
|
|
248
|
+
// The narrow reader is offered only once the gateway bound this session; it never widens the file policy below.
|
|
249
|
+
// 294 C5: a tool granted by the owner is exposed only after the engine ACKed that policy revision (zgw.agent_tool_tools).
|
|
250
|
+
let grantedTools: { revision: number; names: string[] } = { revision: 0, names: [] };
|
|
251
|
+
// The Hub rotates this session's /internal/agent-tools secret on every open (zgw.agent_tool_bind).
|
|
252
|
+
const toolBinding = installAgentToolBinding(pi as unknown as AgentToolBindExtension);
|
|
253
|
+
const gatewayBase = () => process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
254
|
+
// Signed /internal/agent-tools call for one tool call; the identity comes from the Hub bind, never from the model.
|
|
255
|
+
const gateway = (route: AgentToolRoute, toolCallId: string, body: unknown): Promise<unknown> => {
|
|
256
|
+
const peer = route === "request" ? collaborationToolCard(collaboration) : null;
|
|
257
|
+
const context = toolBinding.context(toolCallId, `agent-tool-input:${toolCallId}`, peer?.activationId ?? null);
|
|
258
|
+
if (!context) throw new Error("agent_tool_unbound");
|
|
259
|
+
return agentToolGatewayClient({ context: () => context, base: gatewayBase(),
|
|
260
|
+
token: () => readFileSync(runtimePaths().omoTokenFile, "utf8").trim() })
|
|
261
|
+
.send(route, toolCallId, peer ? { ...(body as Record<string, unknown>), collaborationRequestId: peer.requestId } : body);
|
|
262
|
+
};
|
|
263
|
+
// 294 D1/C7: one adapter per session (it owns the persistent `aside repl` process and the tabs it opened).
|
|
264
|
+
let aside: AsideAdapter | null = null;
|
|
265
|
+
let asideRegistry = new AsideTargetRegistry();
|
|
266
|
+
// D4: owner-bound tabs the gateway carried for granted calls, by the call's stored args; consumed by that execution.
|
|
267
|
+
const carried = new Map<string, SessionAsideTarget>();
|
|
268
|
+
(pi.rpc as unknown as { handle(name: string, handler: (data: unknown) => unknown): void }).handle("zgw.aside_target", (data) => {
|
|
269
|
+
const { key, target } = carriedAsideTarget(data, id);
|
|
270
|
+
if (target === null) carried.delete(key);
|
|
271
|
+
else carried.set(key, target);
|
|
272
|
+
return { ok: true };
|
|
273
|
+
});
|
|
274
|
+
// C8: the one-shot confirmation the gateway carries for an action execution, by the call's stored args (zgw.aside_action).
|
|
275
|
+
// The gateway sends it only after its start transaction consumed the ledger ticket; the execution consumes it once.
|
|
276
|
+
const asideTickets = new ActionTickets();
|
|
277
|
+
const carriedActions = new Map<string, string>();
|
|
278
|
+
(pi.rpc as unknown as { handle(name: string, handler: (data: unknown) => unknown): void }).handle("zgw.aside_action", (data) => {
|
|
279
|
+
const value = data as { args?: unknown; ticket?: unknown } | null;
|
|
280
|
+
if (!value || typeof value !== "object" || !("args" in value) || (value.ticket !== null && (typeof value.ticket !== "string" || value.ticket === ""))) {
|
|
281
|
+
throw new Error("aside_action_invalid");
|
|
282
|
+
}
|
|
283
|
+
const key = canonicalJson(value.args);
|
|
284
|
+
if (value.ticket === null) carriedActions.delete(key);
|
|
285
|
+
else carriedActions.set(key, value.ticket as string);
|
|
286
|
+
return { ok: true };
|
|
287
|
+
});
|
|
288
|
+
// Click/fill calls of this session already sent to the gateway as a confirmation card, by engine operation id.
|
|
289
|
+
const actionRequested = new Set<string>();
|
|
290
|
+
const asideScope = new AsyncLocalStorage<readonly string[]>();
|
|
291
|
+
const asideAdapter = () => aside ??= new AsideAdapter({
|
|
292
|
+
registry: asideRegistry, tickets: asideTickets, host: "local", gatewayOrigins: [new URL(gatewayBase()).origin],
|
|
293
|
+
confirmedAccount: () => asideAccount(asideBin()),
|
|
294
|
+
// The adapter asks per permission class; the call's scope is the grant read for this exact call.
|
|
295
|
+
authorize: (permission) => asidePermits(asideScope.getStore() ?? [], permission),
|
|
296
|
+
});
|
|
297
|
+
pi.registerTool({
|
|
298
|
+
name: ASIDE_TOOL, label: "Aside browser", description: ASIDE_DESCRIPTION, parameters: ASIDE_PARAMETERS,
|
|
299
|
+
async execute(toolCallId, params) {
|
|
300
|
+
// The engine authority already allowed the tool; the operation must also be one the current grant names.
|
|
301
|
+
const policy = await gateway("policy", toolCallId, null);
|
|
302
|
+
const granted = asideGrantedOperations(policy);
|
|
303
|
+
const decision = asideCallDecision(policy, params.operation);
|
|
304
|
+
const key = canonicalJson(params);
|
|
305
|
+
const bound = carried.get(key);
|
|
306
|
+
carried.delete(key);
|
|
307
|
+
const adapter = asideAdapter();
|
|
308
|
+
if (bound) asideRegistry.adopt(bound);
|
|
309
|
+
const input = bound ? withCarriedTarget(params, bound) : params;
|
|
310
|
+
const ctx = { agentId: id, incarnation: toolBinding.lifecycleEpoch() ?? 0 };
|
|
311
|
+
const carriedTicket = carriedActions.get(canonicalJson(input));
|
|
312
|
+
carriedActions.delete(canonicalJson(input));
|
|
313
|
+
let result: unknown;
|
|
314
|
+
if (decision === "refuse") {
|
|
315
|
+
result = { ok: false, refused: "permission_denied" };
|
|
316
|
+
} else if (decision === "hold") {
|
|
317
|
+
result = await holdOperation(toolCallId, ctx, input);
|
|
318
|
+
} else if ((input.operation === "click" || input.operation === "fill") && carriedTicket === undefined) {
|
|
319
|
+
result = await asideScope.run(granted, () => requestAction(toolCallId, ctx, adapter, input));
|
|
320
|
+
} else {
|
|
321
|
+
const actionTicket = carriedTicket === undefined ? undefined : asideTickets.issue(actionDigest(String(input.targetHandle), input.operation as "click" | "fill",
|
|
322
|
+
String(input.ref), String(input.snapshotRevision), input.operation === "fill" ? String(input.text) : null));
|
|
323
|
+
result = await asideScope.run(granted, () => adapter.invoke({ ...ctx, ...(actionTicket ? { actionTicket } : {}) }, input));
|
|
324
|
+
}
|
|
325
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
// Operations of this session already sent to the gateway as a grant card, by engine operation id.
|
|
329
|
+
const heldRequested = new Set<string>();
|
|
330
|
+
/**
|
|
331
|
+
* C1/C7: an operation the current zgw_aside grant does not name. The engine allows zgw_aside by name, so the session holds
|
|
332
|
+
* the call the way the engine holds an ungranted one: recorded once as agent-op:<toolCallId> with its exact input and sent
|
|
333
|
+
* to the gateway as one new grant card. After the owner allows, the gateway executes that operation once and continues.
|
|
334
|
+
*/
|
|
335
|
+
async function holdOperation(toolCallId: string, ctx: { agentId: string; incarnation: number }, input: Record<string, unknown>): Promise<unknown> {
|
|
336
|
+
const operationId = agentToolOperationId(toolCallId);
|
|
337
|
+
if (!heldRequested.has(operationId)) {
|
|
338
|
+
heldRequested.add(operationId);
|
|
339
|
+
pi.appendEntry("agent_tool_operation", { operationId, requestId: `agent-tool-request:${operationId}`, toolName: ASIDE_TOOL, toolCallId, input, path: "direct" });
|
|
340
|
+
}
|
|
341
|
+
const known = typeof input.targetHandle === "string" ? asideRegistry.resolve(input.targetHandle, ctx.agentId, ctx.incarnation) : null;
|
|
342
|
+
const scope = heldAsideScope(known && !("refused" in known) ? known : null);
|
|
343
|
+
const info = pi.getAllTools?.().find((tool) => tool.name === ASIDE_TOOL);
|
|
344
|
+
const source = info?.sourceInfo?.source;
|
|
345
|
+
const sourceKind = source === "builtin" || source === "extension" || source === "mcp" ? source as ToolSourceKind : undefined;
|
|
346
|
+
const { toolKey, operation } = heldToolRequest({ name: ASIDE_TOOL, parameters: info?.parameters ?? null, ...(sourceKind ? { sourceKind } : {}) }, input);
|
|
347
|
+
try {
|
|
348
|
+
const card = await gateway("request", toolCallId, { operation, toolKey, args: input, scope }) as { requestId?: unknown };
|
|
349
|
+
return { ok: false, state: "permission_pending", requestId: card.requestId, operationId };
|
|
350
|
+
} catch (error) {
|
|
351
|
+
heldRequested.delete(operationId);
|
|
352
|
+
return { ok: false, refused: "permission_denied", reason: error instanceof Error ? error.message : "unknown" };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* C8: a click/fill without its confirmation. The call is checked and described (never run), recorded once as the engine
|
|
357
|
+
* operation agent-action:<toolCallId> with its exact input, and sent to the gateway as one agent_tool_action card. After
|
|
358
|
+
* the owner allows, the gateway executes that operation once with the consumed ticket and continues the original work.
|
|
359
|
+
*/
|
|
360
|
+
async function requestAction(toolCallId: string, ctx: { agentId: string; incarnation: number }, adapter: AsideAdapter, input: Record<string, unknown>): Promise<unknown> {
|
|
361
|
+
const prepared = adapter.prepareAction(ctx, input);
|
|
362
|
+
if (!prepared.ok) return prepared;
|
|
363
|
+
const operationId = agentToolActionId(toolCallId);
|
|
364
|
+
if (!actionRequested.has(operationId)) {
|
|
365
|
+
actionRequested.add(operationId);
|
|
366
|
+
pi.appendEntry("agent_tool_operation", { operationId, requestId: `agent-tool-request:${operationId}`, toolName: ASIDE_TOOL, toolCallId, input, path: "operation" });
|
|
367
|
+
}
|
|
368
|
+
const info = pi.getAllTools?.().find((tool) => tool.name === ASIDE_TOOL);
|
|
369
|
+
const source = info?.sourceInfo?.source;
|
|
370
|
+
const sourceKind = source === "builtin" || source === "extension" || source === "mcp" ? source as ToolSourceKind : undefined;
|
|
371
|
+
const { toolKey } = heldToolRequest({ name: ASIDE_TOOL, parameters: info?.parameters ?? null, ...(sourceKind ? { sourceKind } : {}) }, input);
|
|
372
|
+
try {
|
|
373
|
+
const card = await gateway("action", toolCallId, { operation: input.operation, toolKey, args: input, target: { origin: prepared.target.origin, kind: prepared.target.kind }, element: prepared.element }) as
|
|
374
|
+
{ requestId?: unknown; decisionId?: unknown };
|
|
375
|
+
return { ok: false, state: "action_confirmation_pending", targetHandle: prepared.target.handle, requestId: card.requestId, operationId };
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return { ok: false, refused: "action_ticket_required", targetHandle: prepared.target.handle, reason: error instanceof Error ? error.message : "unknown" };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// zgw_aside is offered once the Hub pushed a policy (a managed agent): the engine authority then holds or allows it.
|
|
381
|
+
const activeTools = () => [...new Set([...(attachmentRead.bound() ? [...tools, ATTACHMENT_READ_TOOL] : tools), ...grantedTools.names,
|
|
382
|
+
...(grantedTools.revision >= 1 ? [ASIDE_TOOL] : [])])];
|
|
383
|
+
// D2: a call the engine authority held becomes one gateway request card; the stored operation runs after the allow.
|
|
384
|
+
const requested = new Set<string>();
|
|
385
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
386
|
+
const held = heldOperation(event);
|
|
387
|
+
if (!held || requested.has(held.operationId)) return;
|
|
388
|
+
const entry = ctx.sessionManager.getEntries?.().find((e) => e.type === "custom" && e.customType === "agent_tool_operation" && e.data?.operationId === held.operationId);
|
|
389
|
+
if (!entry?.data || typeof entry.data.toolName !== "string") return;
|
|
390
|
+
requested.add(held.operationId);
|
|
391
|
+
const toolName = entry.data.toolName;
|
|
392
|
+
const info = pi.getAllTools?.().find((tool) => tool.name === toolName);
|
|
393
|
+
const source = info?.sourceInfo?.source;
|
|
394
|
+
const sourceKind = source === "builtin" || source === "extension" || source === "mcp" ? source as ToolSourceKind : undefined;
|
|
395
|
+
const { toolKey, operation } = heldToolRequest({ name: toolName, parameters: info?.parameters ?? null, ...(sourceKind ? { sourceKind } : {}) }, entry.data.input);
|
|
396
|
+
try {
|
|
397
|
+
await gateway("request", event.toolCallId, { operation, toolKey, args: entry.data.input ?? null, scope: null });
|
|
398
|
+
} catch (error) {
|
|
399
|
+
requested.delete(held.operationId);
|
|
400
|
+
warn(`agent_tool_request_failed:${error instanceof Error ? error.message : "unknown"}`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
123
403
|
const skillSnapshots = new Map<string, string>();
|
|
124
404
|
let memoryCall: string | undefined;
|
|
125
405
|
let ui: Context["ui"] | undefined;
|
|
126
406
|
let startupWarnings: { code: string; message: string }[] = [];
|
|
407
|
+
let appliedSha: string | null = null;
|
|
408
|
+
// Daily-note reminder: the dates this session wrote, whether the current turn has user input, and a pending reminder date.
|
|
409
|
+
const dailyWritten = new Set<string>();
|
|
410
|
+
// Admitted writes of today's note, by tool call id, until the tool reports the outcome.
|
|
411
|
+
const dailyCalls = new Map<string, string>();
|
|
412
|
+
let userInput = false;
|
|
413
|
+
let remind: string | undefined;
|
|
127
414
|
// session_start precedes the gateway's open_session attachment. Read this snapshot after binding the owner.
|
|
128
|
-
pi.rpc.handle("zgw.agent_warnings", () => ({ warnings: startupWarnings }));
|
|
415
|
+
pi.rpc.handle("zgw.agent_warnings", () => ({ warnings: startupWarnings, appliedSha }));
|
|
416
|
+
// The runtime passes the request data; the declared zero-argument shape predates data handlers (292 widens it).
|
|
417
|
+
(pi.rpc as unknown as { handle(name: string, handler: (data: unknown) => unknown): void }).handle("zgw.agent_tool_tools", (data) => {
|
|
418
|
+
if (!record(data) || !Number.isSafeInteger(data.revision) || (data.revision as number) < 1 || !Array.isArray(data.names) ||
|
|
419
|
+
!data.names.every((name): name is string => typeof name === "string" && name !== "")) throw new Error("agent_tool_tools_invalid");
|
|
420
|
+
// An older revision never replaces a newer one, so a delayed push cannot re-expose a revoked tool.
|
|
421
|
+
if ((data.revision as number) < grantedTools.revision) return { revision: grantedTools.revision, applied: false };
|
|
422
|
+
grantedTools = { revision: data.revision as number, names: [...data.names] };
|
|
423
|
+
if (ready) pi.setActiveTools(activeTools());
|
|
424
|
+
return { revision: grantedTools.revision, applied: true };
|
|
425
|
+
});
|
|
129
426
|
const warn = (code: string) => {
|
|
130
427
|
const data = { agentId: id, code };
|
|
131
428
|
const message = code === "memory_over_cap" ? `MEMORY.md가 크기 상한을 넘어 이번 세션에서 생략되었습니다. ${MEMORY_CAP_MESSAGE}` : `에이전트 요청이 제한되었습니다: ${code}`;
|
|
@@ -134,12 +431,14 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
134
431
|
pi.rpc.emit("zgw.agent_warning", data);
|
|
135
432
|
ui?.notify(message, "warning");
|
|
136
433
|
};
|
|
137
|
-
|
|
434
|
+
// scope=false checks a lexical path: the write area is judged on the canonical target, so a link to MEMORY.md still works.
|
|
435
|
+
const pathDenial = (path: string, write: boolean, scope = true): string | undefined => {
|
|
138
436
|
if (!ready) return "agent_not_ready";
|
|
139
|
-
if (!inside(root, path)) return
|
|
437
|
+
if (!inside(root, path)) return OUTSIDE_KNOWLEDGE_REASON;
|
|
140
438
|
if (!write) return undefined;
|
|
141
439
|
if (inside(join(root, "source"), path)) return "agent_source_read_only";
|
|
142
440
|
if (path === root || inside(join(root, ".lock"), path) || inside(join(root, ".lock.claim"), path)) return "agent_lock_protected";
|
|
441
|
+
if (scope && !writable(root, path)) return WRITE_SCOPE_REASON;
|
|
143
442
|
if (!claimLock(root, sessionId)) return "agent_knowledge_locked";
|
|
144
443
|
return undefined;
|
|
145
444
|
};
|
|
@@ -150,8 +449,14 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
150
449
|
} catch { return { allow: false, reason: "agent_path_unavailable" }; }
|
|
151
450
|
} });
|
|
152
451
|
pi.on("session_start", async (_event, ctx) => {
|
|
452
|
+
collaboration?.start(ctx);
|
|
153
453
|
ready = false;
|
|
454
|
+
appliedSha = null;
|
|
154
455
|
startupWarnings = [];
|
|
456
|
+
dailyWritten.clear();
|
|
457
|
+
dailyCalls.clear();
|
|
458
|
+
userInput = false;
|
|
459
|
+
remind = undefined;
|
|
155
460
|
ui = ctx.ui;
|
|
156
461
|
pi.setActiveTools([]);
|
|
157
462
|
const token = readFileSync(runtimePaths().omoTokenFile, "utf8").trim();
|
|
@@ -175,7 +480,7 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
175
480
|
sessionId = ctx.sessionManager.getSessionId();
|
|
176
481
|
maxBytes = memory.maxBytes;
|
|
177
482
|
skills = allowedSkills;
|
|
178
|
-
tools = [...BASE_TOOLS, ...(toolSettings.web === true ? ["web_search", "webfetch"] : [])];
|
|
483
|
+
tools = [...BASE_TOOLS, ...COLLABORATION_TOOLS, ...(toolSettings.web === true ? ["web_search", "webfetch"] : [])];
|
|
179
484
|
const sections: string[] = [];
|
|
180
485
|
for (const [enabled, path, heading] of [
|
|
181
486
|
[rules.gateway, PACKAGE_RULES_FILE, PACKAGE_RULES_HEADING],
|
|
@@ -196,19 +501,46 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
196
501
|
else sections.push(`<agent-memory>\n${snapshot.toString("utf8")}\n</agent-memory>`);
|
|
197
502
|
}
|
|
198
503
|
}
|
|
199
|
-
|
|
504
|
+
promptParts = sections;
|
|
200
505
|
if (!claimLock(root, sessionId)) warn("knowledge_locked");
|
|
506
|
+
// Agents made before outputs/ existed get the folder from the lock owner.
|
|
507
|
+
else {
|
|
508
|
+
try { mkdirSync(join(root, "outputs"), { recursive: true }); } catch { warn("outputs_unavailable"); }
|
|
509
|
+
}
|
|
201
510
|
ready = true;
|
|
511
|
+
if (collaboration) {
|
|
512
|
+
const sdk = await sdkForSession();
|
|
513
|
+
fileBoundary = new CollaborationSnapshots({ root: realpathSync(join(runtimeHome(), "agents")), control: collaboration,
|
|
514
|
+
queue: sdk.withFileMutationQueue, assertKnowledgeLock() {
|
|
515
|
+
if (!ready || collaboration.capture().binding.agentId !== id || !claimLock(root, sessionId)) throw new CollaborationExtensionError("agent_knowledge_locked");
|
|
516
|
+
} });
|
|
517
|
+
fileBoundary.install(pi, { sdk, cwd: root, policy: async ({ operation, canonicalPath }) => {
|
|
518
|
+
const reason = pathDenial(canonicalPath, operation === "write");
|
|
519
|
+
return reason ? { allow: false, reason } : { allow: true };
|
|
520
|
+
} });
|
|
521
|
+
}
|
|
522
|
+
appliedSha = typeof definition.sha === "string" ? definition.sha : null;
|
|
523
|
+
collaboration?.applied({ definitionSha: appliedSha ?? "", fingerprint: createHash("sha256").update(JSON.stringify({ prompt, model: ctx.model, thinking: ctx.thinkingLevel, appliedSha })).digest("hex") });
|
|
202
524
|
pi.setActiveTools(tools);
|
|
203
525
|
});
|
|
204
|
-
pi.on("session_shutdown", () => {
|
|
526
|
+
pi.on("session_shutdown", () => {
|
|
527
|
+
collaboration?.revoke();
|
|
528
|
+
if (root && sessionId) claimLock(root, sessionId, true);
|
|
529
|
+
ready = false;
|
|
530
|
+
void aside?.shutdown("session_shutdown");
|
|
531
|
+
aside = null;
|
|
532
|
+
asideRegistry = new AsideTargetRegistry();
|
|
533
|
+
carried.clear();
|
|
534
|
+
});
|
|
535
|
+
pi.on("agent_settled", () => { collaboration?.settle(); });
|
|
205
536
|
const requestRules = loadRequestRules();
|
|
206
537
|
pi.on("before_provider_request", (event) => applyRequestRules(event, requestRules));
|
|
207
538
|
let profile: PromptProfile = { nickname: undefined, agentName: "" };
|
|
208
539
|
pi.on("before_agent_start", async (event) => {
|
|
209
540
|
if (!ready) throw new Error("agent_not_ready");
|
|
541
|
+
if (collaboration?.isPeer()) userInput = false;
|
|
210
542
|
[profile, agentName] = await Promise.all([readPromptProfile(profile), readAgentPromptName(id, agentName)]);
|
|
211
|
-
pi.setActiveTools(
|
|
543
|
+
pi.setActiveTools(activeTools());
|
|
212
544
|
// File tools remain confined to knowledge. Approved skill bodies are loaded here, not through a read exception.
|
|
213
545
|
for (const skill of event.systemPromptOptions.skills ?? []) {
|
|
214
546
|
if (skills.includes(skill.name) && !skillSnapshots.has(skill.name)) skillSnapshots.set(skill.name, readFileSync(skill.filePath, "utf8"));
|
|
@@ -216,31 +548,57 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
216
548
|
const base = event.systemPrompt.replace(/<available_skills>[\s\S]*?<\/available_skills>/g, "")
|
|
217
549
|
.replace(/<skill_roots>[\s\S]*?<\/skill_roots>/g, "");
|
|
218
550
|
const selected = [...skillSnapshots].map(([name, text]) => `<agent-skill name="${name}">\n${text}\n</agent-skill>`).join("\n\n");
|
|
219
|
-
|
|
551
|
+
let files = "";
|
|
552
|
+
// A broken listing must not stop the turn; the warning names the cause and the block is left out.
|
|
553
|
+
try { files = filesBlock(root, collaboration?.isPeer() ? fileBoundary?.outputRoot() ?? null : root); } catch { warn("knowledge_listing_failed"); }
|
|
554
|
+
// A reminder raised on an earlier KST day is dropped; it is shown once.
|
|
555
|
+
// C1: write instructions only when this agent may write (legacy, or a managed agent with write/edit granted).
|
|
556
|
+
const writes = writeInstructionsAllowed(grantedTools);
|
|
557
|
+
const prompt = promptParts.filter((part) => writes || (part !== DAILY_RULES && part !== RESEARCH_RULES)).join("\n\n");
|
|
558
|
+
const reminder = writes && remind === kstDate(now()) ? dailyReminder(remind) : "";
|
|
559
|
+
remind = undefined;
|
|
560
|
+
// The nickname line is the agent variant: the role instructions in `prompt` may set another form of address (todo 9).
|
|
561
|
+
return { systemPrompt: [base, prompt, selected, files, agentNamePrompt(agentName), agentNicknamePrompt(profile.nickname), reminder].filter(Boolean).join("\n\n") };
|
|
220
562
|
});
|
|
221
563
|
pi.on("input", (event) => {
|
|
222
564
|
if (!ready) { warn("agent_not_ready"); return { action: "handled" }; }
|
|
565
|
+
if (collaboration?.input(event)) { userInput = false; return { action: "continue" }; }
|
|
223
566
|
const known = new Set(pi.getCommands().filter((command) => command.source === "skill").map((command) => command.name.replace(/^skill:/, "")));
|
|
224
567
|
for (const match of event.text.matchAll(/(?:^|\s)(\/skill:|\$(?:skill:)?)([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s|$)/g)) {
|
|
225
568
|
const name = match[2] ?? "";
|
|
226
569
|
if (known.has(name) && !skills.includes(name)) { warn("skill_not_allowed"); return { action: "handled" }; }
|
|
227
570
|
}
|
|
571
|
+
userInput = true;
|
|
228
572
|
return { action: "continue" };
|
|
229
573
|
});
|
|
230
|
-
pi.on("tool_call", async (event) => {
|
|
231
|
-
if (!ready || !
|
|
574
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
575
|
+
if (!ready || !activeTools().includes(event.toolName)) return blocked("agent_tool_not_allowed");
|
|
576
|
+
try {
|
|
577
|
+
if (collaboration?.isPeer()) collaboration.observeClientInput();
|
|
578
|
+
if (!COLLABORATION_TOOLS.includes(event.toolName)) collaboration?.assertGeneralTool();
|
|
579
|
+
}
|
|
580
|
+
catch (error) { if (error instanceof CollaborationExtensionError) return blocked(error.code); throw error; }
|
|
232
581
|
if (!["read", "write", "edit"].includes(event.toolName)) return;
|
|
582
|
+
if (collaboration?.isPeer()) return;
|
|
233
583
|
try {
|
|
234
584
|
if (typeof event.input.path !== "string" || event.input.path === "") return blocked("agent_path_required");
|
|
585
|
+
const write = event.toolName !== "read";
|
|
586
|
+
// Judge the argument as given: resolve() would remove a .. segment before the area checks see it.
|
|
587
|
+
if (write && event.input.path.split(/[\\/]/).includes("..")) return blocked(WRITE_DOTDOT_REASON);
|
|
235
588
|
const expanded = event.input.path.replace(/^~(?=\/|$)/, homedir());
|
|
236
589
|
const lexical = resolve(root, expanded);
|
|
237
590
|
const path = agentPath(lexical);
|
|
238
|
-
const
|
|
239
|
-
|
|
591
|
+
const reason = pathDenial(lexical, write, false) ?? pathDenial(path, write);
|
|
592
|
+
// A managed session (the Hub opened it full-access) has no native external_directory prompt; the owner sees this one.
|
|
593
|
+
if (reason === OUTSIDE_KNOWLEDGE_REASON && pi.sessionContext.zgw_tool_authority === "managed") {
|
|
594
|
+
await ctx.ui.select(outsideKnowledgePrompt(lexical, path), ["Deny"], { toolCallId: event.toolCallId });
|
|
595
|
+
}
|
|
240
596
|
if (reason) return blocked(reason);
|
|
241
597
|
if (write && existsSync(path) && lstatSync(path).nlink > 1) return blocked("agent_linked_file_read_only");
|
|
242
598
|
// Canonicalize the actual argument too; do not leave a checked symlink for the native tool to follow.
|
|
243
599
|
event.input.path = path;
|
|
600
|
+
const today = kstDate(now());
|
|
601
|
+
if (write && path === dailyPath(root, today)) dailyCalls.set(event.toolCallId, today);
|
|
244
602
|
if (!write || path !== agentPath(join(root, "MEMORY.md"))) return;
|
|
245
603
|
if (memoryCall !== undefined) return blocked("agent_memory_busy");
|
|
246
604
|
memoryCall = event.toolCallId;
|
|
@@ -252,7 +610,7 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
252
610
|
const edits = event.input.edits;
|
|
253
611
|
if (!Array.isArray(edits) || !edits.every((e): e is Edit => record(e) && typeof e.oldText === "string" && typeof e.newText === "string")) throw new Error("agent_edit_input_invalid");
|
|
254
612
|
// Run the pinned native editor against memory-only operations: BOM, CRLF, fuzzy and multi-edit semantics stay exact.
|
|
255
|
-
const sdk = await
|
|
613
|
+
const sdk = await sdkForSession();
|
|
256
614
|
let result: string | undefined;
|
|
257
615
|
await sdk.createEditTool(root, { operations: {
|
|
258
616
|
readFile: async () => readFileSync(path), access: async () => {},
|
|
@@ -267,8 +625,22 @@ export default function agentControl(pi: ExtensionApi, loadSdk: () => Promise<Ag
|
|
|
267
625
|
return blocked(error instanceof Error ? error.message : "agent_path_unavailable");
|
|
268
626
|
}
|
|
269
627
|
});
|
|
270
|
-
pi.on("tool_result", (event) => {
|
|
628
|
+
pi.on("tool_result", (event) => {
|
|
629
|
+
if (memoryCall === event.toolCallId) memoryCall = undefined;
|
|
630
|
+
const date = dailyCalls.get(event.toolCallId);
|
|
631
|
+
dailyCalls.delete(event.toolCallId);
|
|
632
|
+
// Only a write the tool reports as done counts as today's note; a failed write keeps the reminder.
|
|
633
|
+
if (date !== undefined && event.isError === false) { dailyWritten.add(date); remind = undefined; }
|
|
634
|
+
});
|
|
271
635
|
// A later hook/permission can refuse a call without a tool_result. Its turn end also releases the reservation.
|
|
272
|
-
pi.on("agent_end", () => {
|
|
273
|
-
|
|
636
|
+
pi.on("agent_end", () => {
|
|
637
|
+
memoryCall = undefined;
|
|
638
|
+
// A call refused after admission has no tool_result; it never counts, also not by a late result.
|
|
639
|
+
dailyCalls.clear();
|
|
640
|
+
// A user turn that ends without a write to today's note raises the reminder for the next turn.
|
|
641
|
+
const today = kstDate(now());
|
|
642
|
+
if (userInput && !dailyWritten.has(today)) remind = today;
|
|
643
|
+
userInput = false;
|
|
644
|
+
});
|
|
645
|
+
pi.on("session_abort", () => { collaboration?.revoke(); memoryCall = undefined; });
|
|
274
646
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
;; 294 item 6: OS profile for a registered agent's bash/powershell/command monitor.
|
|
2
|
+
;; Default deny. Only the interpreter, system libraries and the work root (a copy of approved files) are readable.
|
|
3
|
+
;; Only the work root is writable. Network, sockets, Apple Events and ptrace stay denied.
|
|
4
|
+
;; Parameters: WORK_ROOT (real path of the copy root), INTERPRETER (real path), INTERPRETER_DIR (its folder).
|
|
5
|
+
(version 1)
|
|
6
|
+
(deny default)
|
|
7
|
+
|
|
8
|
+
(allow process-fork)
|
|
9
|
+
(allow process-exec
|
|
10
|
+
(literal (param "INTERPRETER"))
|
|
11
|
+
(subpath "/bin")
|
|
12
|
+
(subpath "/usr/bin"))
|
|
13
|
+
(allow signal (target same-sandbox))
|
|
14
|
+
(allow sysctl-read)
|
|
15
|
+
|
|
16
|
+
;; Path lookups (dyld, shells) need stat on parent folders; file contents stay denied below.
|
|
17
|
+
(allow file-read-metadata)
|
|
18
|
+
(allow file-read*
|
|
19
|
+
(literal "/")
|
|
20
|
+
(subpath "/usr/lib")
|
|
21
|
+
(subpath "/usr/share")
|
|
22
|
+
(subpath "/System")
|
|
23
|
+
(subpath "/bin")
|
|
24
|
+
(subpath "/usr/bin")
|
|
25
|
+
(subpath "/private/var/db/dyld")
|
|
26
|
+
(subpath "/Library/Apple/System")
|
|
27
|
+
(literal "/dev/null")
|
|
28
|
+
(literal "/dev/zero")
|
|
29
|
+
(literal "/dev/random")
|
|
30
|
+
(literal "/dev/urandom")
|
|
31
|
+
(literal "/dev/tty")
|
|
32
|
+
(subpath (param "INTERPRETER_DIR"))
|
|
33
|
+
(subpath (param "WORK_ROOT")))
|
|
34
|
+
(allow file-write*
|
|
35
|
+
(literal "/dev/null")
|
|
36
|
+
(subpath (param "WORK_ROOT")))
|
|
37
|
+
(allow file-ioctl (literal "/dev/null") (literal "/dev/tty"))
|
|
38
|
+
|
|
39
|
+
(deny network*)
|
|
40
|
+
(deny appleevent-send)
|
|
41
|
+
(deny process-info* (target others))
|
|
42
|
+
(deny mach-priv-task-port)
|