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
package/docs/manual.md
CHANGED
|
@@ -19,7 +19,7 @@ zigmini has two surfaces.
|
|
|
19
19
|
|
|
20
20
|
The web screen has three parts.
|
|
21
21
|
|
|
22
|
-
- Left sidebar: the default agent (main) conversation, the agent group with one conversation per agent, the work status area, your profile and the default agent profile. The "
|
|
22
|
+
- Left sidebar: the default agent (main) conversation, the agent group with one conversation per agent, the work status area, your profile and the default agent profile. While an agent conversation is open, the profile shows that agent's name and icon instead. The "Engine" badge at the top shows the engine connection.
|
|
23
23
|
- Center chat: the header, the message list and the input box.
|
|
24
24
|
- Right panels: the inbox, settings and the event panel open here.
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ model or saved model profiles; an installed gateway needs an update to receive i
|
|
|
34
34
|
| --- | --- |
|
|
35
35
|
| Zig face button | Shows work status. At phone width it switches to the work status view. |
|
|
36
36
|
| Model picker | Picks the model the agent uses. If the switch can't happen right away, it happens after the conversation is compacted on your next message. |
|
|
37
|
-
|
|
|
37
|
+
| New session | Starts a fresh agent session for this conversation. A confirmation comes first. The history on screen stays, and part of the recent conversation is handed to the new session. In an agent conversation whose definition changed, the button is highlighted and reads "Apply with new session". |
|
|
38
38
|
| 음성 켜짐 / 음성 꺼짐 (voice on/off) | Turns the gateway's voice servers on or off. With voice off, the mic, wake word, clap detection and spoken answers are unavailable. |
|
|
39
39
|
| 음성 설치 (install voice) | Shown only on an npm install without voice. It downloads and installs the voice servers. |
|
|
40
40
|
| 박수 감지 (clap detection) | Two claps turn wake word listening on or off. |
|
|
@@ -134,8 +134,12 @@ at the same depth follow their input order.
|
|
|
134
134
|
Unfinished, invalid or unsupported blocks remain visible as source. When a streamed
|
|
135
135
|
block becomes complete and valid, chat can render it. Diagram text cannot run
|
|
136
136
|
scripts, load URLs or read files. An older client displays the code block instead.
|
|
137
|
-
The
|
|
138
|
-
a
|
|
137
|
+
The per-request instruction says that people understand faster when they can see it.
|
|
138
|
+
When a reply explains a flow (order, steps, state changes), a structure (components
|
|
139
|
+
and their relations) or a cause and its result, the agent adds a diagram in this
|
|
140
|
+
`zigmini-diagram` format. Other replies do not require a diagram. When a reply
|
|
141
|
+
compares or summarizes several items by the same criteria (for example current
|
|
142
|
+
state, problem, fix direction), the agent uses a table.
|
|
139
143
|
|
|
140
144
|
#### Images
|
|
141
145
|
|
|
@@ -145,6 +149,44 @@ a flow or structure; it does not require diagrams in every reply.
|
|
|
145
149
|
|
|
146
150
|
You can send up to 4 images at once: PNG, JPEG, GIF or WebP. Images upload when you send.
|
|
147
151
|
|
|
152
|
+
#### Files on this Mac
|
|
153
|
+
|
|
154
|
+
Type `@` at the start of the input box or after a space, or click **Mac files**. The **Files on this Mac** picker lists files on the Mac that runs the gateway. It never lists files on your browser's computer or on your phone. There's no extra folder or device to set up. An `@` inside a word, such as an email address, stays plain text, and so do `\@` and `@@`.
|
|
155
|
+
|
|
156
|
+
The picker opens in `~/Downloads` when it can, and **Home** and **Computer** take you elsewhere. Search covers the current folder only. Hidden files stay out of the list until you turn on **Show hidden files**. Links can't be selected.
|
|
157
|
+
|
|
158
|
+
| Item | Limit |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| Formats | `.html`, `.htm`, `.md`, `.markdown`, `.txt`, `.pdf` |
|
|
161
|
+
| File size | 10 MiB per file, not empty |
|
|
162
|
+
| PDF length | 300 pages |
|
|
163
|
+
| One message | 4 different documents, counting documents from referenced messages |
|
|
164
|
+
| Selected but not sent | 16 files and 64 MiB per web or phone client |
|
|
165
|
+
|
|
166
|
+
Images keep using the separate image button.
|
|
167
|
+
|
|
168
|
+
The gateway saves a private copy of the file the moment you select it. That copy can't change, so editing the source file afterwards has no effect on it. To send a newer version, select the file again. The gateway never moves or changes the source file.
|
|
169
|
+
|
|
170
|
+
Each file shows its progress on a chip above the input box. Send waits until every file is **Ready**. If one fails, retry it or remove it. A text file that isn't UTF-8 asks you to **Choose encoding**.
|
|
171
|
+
|
|
172
|
+
Selecting a file gives no agent access. **Send** lets only the receiving conversation's agent read it. Other conversations and agent clones get nothing, and the file isn't copied into an agent's knowledge or memory. Agents read it with a narrow tool, `zgw_attachment_read`, which can open only the files granted to that conversation. It can't take a path, so an agent can't use it to browse the Mac.
|
|
173
|
+
|
|
174
|
+
How a PDF reaches the model depends on the model in use. The gateway decides this for you and doesn't switch models:
|
|
175
|
+
|
|
176
|
+
| Model route | What the model receives | Chip label |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| Public OpenAI or Anthropic API with PDF input | The PDF itself | **PDF input** |
|
|
179
|
+
| ChatGPT or Claude subscription | Page text plus page images | **Page images** |
|
|
180
|
+
| Text-only model | Page text plus OCR on this Mac | **OCR text** |
|
|
181
|
+
|
|
182
|
+
The gateway keeps your original copy, and a sent file's name in the message downloads it. HTML is only downloaded, never opened as a page.
|
|
183
|
+
|
|
184
|
+
To revoke a sent file, press **Revoke** on that file in your sent message and confirm. The file then shows **Unavailable**, whether its delivery was still pending, and what revocation does not take back. Revoking a sent file denies future reads and future downloads. It can't erase what a model has already read, files you've already downloaded, or anything made from the file. Cleanup happens only when you ask for it and moves the files to the macOS Trash. Nothing is deleted automatically.
|
|
185
|
+
|
|
186
|
+
If a row or chip says **Mac access denied**, macOS privacy protection blocked that location. Other locations keep working. See 5.4.
|
|
187
|
+
|
|
188
|
+
An older gateway shows **This gateway does not support Mac file attachments.** An older app that receives a sent file shows its name as `[Attached: name]` text.
|
|
189
|
+
|
|
148
190
|
#### Referencing earlier messages
|
|
149
191
|
|
|
150
192
|
Right-click a message to attach it as a reference tag above the input box. The message's 참조 (reference) button does the same. You can add up to 4 references. Images from referenced messages plus new attachments can't go over 4 in one send. To drop a tag, click its button, or focus it and press Delete or Backspace.
|
|
@@ -298,7 +340,7 @@ A QR for the Android app (APK) shows. Scan it with the phone camera, get the APK
|
|
|
298
340
|
|
|
299
341
|
#### Cleanup (정리)
|
|
300
342
|
|
|
301
|
-
The gateway lists things that are safe to
|
|
343
|
+
The gateway shows two groups. "Work state" lists unfinished works that the gateway closed on evidence or that need a decision; these rows are read only. A row that needs a decision shows the note "Main will ask you to confirm", and a row the gateway closed shows "Closed from evidence". "Reclaim" lists things that are safe to remove: DB backups, engine version folders, leftover update files, temp folders, archived agent folders, merged worktrees and old gateway Docker images. Tick the ones to remove and click "Clean the selected items". Files and folders go to the macOS Trash. Docker images carry the "Cannot be undone" badge. "Select all" ticks every row you can pick, or clears them all; information rows such as the Docker build cache size can't be picked. "View log" shows the last 20 cleanup records. Old RPC daemons and orphaned test processes are stopped automatically. Details: [Housekeeping](operator.md#housekeeping).
|
|
302
344
|
|
|
303
345
|
#### Voice settings
|
|
304
346
|
|
|
@@ -331,16 +373,47 @@ Terms: the default agent (main, Zig) is the conversation the gateway has from th
|
|
|
331
373
|
{"version":1,"title":"Default agent, agents and work sessions","nodes":[{"id":"user","label":"You"},{"id":"main","label":"Default agent (main, Zig)"},{"id":"agent","label":"Agent conversation"},{"id":"work","label":"Work"},{"id":"knowledge","label":"Agent knowledge folder"}],"edges":[{"from":"user","to":"main"},{"from":"user","to":"agent"},{"from":"main","to":"work"},{"from":"agent","to":"knowledge"}]}
|
|
332
374
|
```
|
|
333
375
|
|
|
376
|
+
The usual flow: create the agent, upload its material, talk with it, then download what it made.
|
|
377
|
+
|
|
378
|
+
```zigmini-diagram
|
|
379
|
+
{"version":1,"title":"From a new agent to its outputs","nodes":[{"id":"create","label":"Create (+ Agent)"},{"id":"upload","label":"Upload to knowledge/source/"},{"id":"chat","label":"Talk in the agent conversation"},{"id":"outputs","label":"Download from knowledge/outputs/"}],"edges":[{"from":"create","to":"upload"},{"from":"upload","to":"chat"},{"from":"chat","to":"outputs"}]}
|
|
380
|
+
```
|
|
381
|
+
|
|
334
382
|
- **Sidebar.** Agents appear in their own group. The **⋯** button beside the group name (or **Agents** at the top of the chat) opens the Agents screen with three views: **List**, **Shared defaults** and **Archive**. You create and edit agents only on this screen: **+ Agent** in **List** creates one, and **Edit** on a row opens its editor. The sidebar and the chat header have no add or edit button. Each agent has exactly one conversation.
|
|
335
|
-
- **
|
|
336
|
-
- **
|
|
383
|
+
- **Conversation.** Replies in an agent conversation carry the agent's name and icon. An agent without an icon shows the first letter of its name.
|
|
384
|
+
- **Editor.** **Name** and **One-line purpose** are required. Optional fields: **Icon**, **Model** (**Provider** and **Model ID**, both filled or both empty; empty means the gateway's current model), **Thinking level**, **Allowed skills**, **Web research (web_search, webfetch)**, **App notification when the reply ends**, the two rule switches **Shared rules** and **Override rules**, and **MEMORY.md limit (bytes)** (0 to 32768). There's no schema version field; the gateway writes it for you. **Shell (prohibited)** and **Code execution (prohibited)** show as switches that stay off, with the reason: shell and code execution bypass the knowledge folder path checks. Each field shows where its value comes from: package default, shared default or this agent. The body is the **Role instructions**. Above it, **How to write role instructions** lists four parts: **Scope**, **Tasks**, **Not to do** and **Form of address**. Write "Call the user ○○" there to change how the agent addresses you; otherwise the profile nickname rule applies.
|
|
385
|
+
- **Allowed skills.** Pick skills from a checklist of the skills installed in the engine. A saved skill that isn't installed stays checked with the mark **not installed**; saving keeps it, and unchecking removes it. If the list can't load, **Try again** appears and the saved values are kept.
|
|
386
|
+
- **What this agent can and cannot do.** A box at the top of the editor lists what the agent **Can** and **Cannot** do with the values you're editing. The gateway computes it as a preview, and it changes as you edit (for example, **Web research (web_search, webfetch)** moves between the two columns with its switch).
|
|
387
|
+
- **Drafts while creating.** While you fill in a new agent, the gateway saves a draft as you type and shows **Draft saved · {time}**. **Reopen a draft ({count})** in **List** brings it back, also from another device. A draft keeps picked files' names and sizes, not their contents, so pick the files again. If saving fails while the editor is closed or reopening, reopening on the same page keeps the latest local text and file list instead of loading an older saved copy. The draft status explains the failure. Check the connection and draft size, then edit to retry saving. Keep this page open without refreshing until **Draft saved · {time}** appears; another page or device can read only the last successful save. Creating the agent closes its draft. Old drafts show up later in **Cleanup** in Settings.
|
|
388
|
+
- **Changes apply from the next session.** After you edit an agent, its header shows the status **⚠ The definition changed** with the note "Press New session to apply the changed definition". The open session keeps the old definition. The **New session** button at the top right is highlighted and reads **Apply with new session**; press it and confirm to start a new session with the edited definition. If the gateway cannot tell which definition the open session applied, the status reads **Cannot confirm whether the definition was applied**, and the button is highlighted the same way; a new session applies the current one. Saving a real change also shows **Apply now** in the editor; after you confirm, it opens the new session at once. Saving without a change says **Nothing changed in the definition.**
|
|
337
389
|
- **Skills.** The "/" menu in an agent conversation hides skills outside its allow list.
|
|
338
|
-
- **Knowledge.** The
|
|
339
|
-
- **
|
|
340
|
-
- **
|
|
341
|
-
- **
|
|
390
|
+
- **Knowledge files.** The editor groups the files by location: **Uploaded material** in `knowledge/source/`, **Outputs** in `knowledge/outputs/`, and **Notes** in `notes/, MEMORY.md` (read only). Drop files on the upload area or press **Choose files**; you can pick several files at once, and each row shows its progress. Uploads are saved at once and the agent uses them right away. No new session is needed: the agent gets the list of its files at every turn. If a file with the same name exists, the new one is saved as "name (2)". Files picked on the create screen show **Uploads after creation** and are uploaded right after the agent is created, and the editor of the new agent then shows their progress.
|
|
391
|
+
- **What the agent writes.** The agent reads every file in its knowledge folder but writes only to `outputs/`, `notes/` and `MEMORY.md`; it can't change `source/`. Ask it to save results to `outputs/`. Each file in **Outputs** and **Uploaded material** has **Download**. With web research on, research notes go under `notes/` with the source URL beside each fact. An agent can search only its own conversation history.
|
|
392
|
+
- **Daily notes.** The agent writes `notes/daily/<date>.md` on each day it works. If today's note is missing after a turn, the agent gets a reminder at its next turn. **Notes** shows **Last daily note: {date}**.
|
|
393
|
+
- **Deleting files.** **Delete** on a file in `source/` or `outputs/` moves it to **Deleted files ({count})**, where **Restore** brings it back for 30 days. After that, **Cleanup** in Settings lists it as a candidate, and it goes to the macOS Trash only after you confirm there. There's no replace button: delete the old file and upload the new one.
|
|
394
|
+
- **Duplicate.** **Duplicate** copies the definition, source files and MEMORY.md into a new agent named "<name> copy". The conversation isn't copied.
|
|
395
|
+
- **Delete and restore.** To delete, type the agent's exact name. The conversation is hidden, and the definition and knowledge move to **Archive** for 30 days. After that, **Cleanup** in Settings offers them for the macOS Trash. You can't delete during a running turn or an open question. **Restore** in **Archive** brings the conversation back. If an agent with the same ID already exists (the same agent was deleted twice and one copy was restored), the row explains the conflict and offers **Restore under a new ID**, which keeps both. An existing agent is never overwritten.
|
|
396
|
+
- **Tour.** On first use, a short tour walks through the input box, the sidebar, **New session**, the inbox, **Agents**, **+ Agent** and Settings. Most steps move on when you use the highlighted part: type one character in the input box (nothing is sent) or click the highlighted control. The **New session** step uses the **Next** button instead, because a real click would start a new engine session; a step whose control is missing or disabled also shows **Next**, and a step whose target isn't on the screen is skipped. **Skip** or Esc ends the whole tour. The tour starts once per browser. **Show the tour again** under **Guided tour** in Settings replays it.
|
|
397
|
+
- **Default agent.** Zig can list agents (name, purpose, link) and can send them collaboration requests (see 2.13). Trashed agents aren't listed.
|
|
342
398
|
- **Android.** The app stays on the default agent (main) conversation. Agent conversations are web only for now.
|
|
343
399
|
|
|
400
|
+
#### Tool permissions
|
|
401
|
+
|
|
402
|
+
A new agent, or a duplicate, starts with no tool access. When it needs a tool, it asks for it, and the request appears in the inbox as a **Tool access request** card. The card shows **Agent**, **Request source** (**Direct request** or a request from a collaborating agent), **Tool**, **Target**, **Operation** and **Duration** (**Until revoked**).
|
|
403
|
+
|
|
404
|
+
```zigmini-diagram
|
|
405
|
+
{"version":1,"title":"Tool access from request to result","nodes":[{"id":"need","label":"Agent asks for a tool"},{"id":"check","label":"Gateway checks existing permissions"},{"id":"inbox","label":"Tool access request in the inbox"},{"id":"grant","label":"Allow this scope: permission is kept"},{"id":"deny","label":"Do not allow: nothing runs"},{"id":"run","label":"Recheck, then run once"},{"id":"result","label":"Agent gets the result and continues"},{"id":"edit","label":"Edit scope or Revoke access"}],"edges":[{"from":"need","to":"check"},{"from":"check","to":"inbox"},{"from":"check","to":"run"},{"from":"inbox","to":"grant"},{"from":"inbox","to":"deny"},{"from":"grant","to":"run"},{"from":"run","to":"result"},{"from":"grant","to":"edit"}]}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- **Persistent permission.** **Allow this scope** keeps the permission for that agent, tool, operation and target until you revoke it. The agent won't ask again for the same target and operation. It doesn't spread to another tool, another agent or another tab. After you allow it, the gateway runs the waiting operation once and the agent continues its original task with the result. **Do not allow** runs nothing ("Access was not granted. Nothing was run."), and the agent won't repeat the same request unless you ask for something new.
|
|
409
|
+
- **Editing and revoking.** The agent editor has a **Tool permissions** section. Each row shows the tool, operations, target, state (**Allowed**, **Applying**, **Revoked**, **Target needs confirmation** or **No access**) and the original confirmation. Turning a row's switch off revokes it at once; open sessions stop using it too, and no new session is needed. **Edit scope** narrows a scope at once. Widening one sends a new request to the inbox ("Approve the added scope"), and only the current scope applies until you allow it. **Revoke access** removes one permission. If an operation is running, the gateway asks it to stop and shows "Access revoked. Stopping the running operation." until the stop is confirmed; it can't undo what already happened. **Revoke all tool access** revokes every permission of the agent and blocks new requests. To use tools again, allow a new request in the inbox; old permissions don't come back.
|
|
410
|
+
- **External actions need their own confirmation.** A tool permission lets the agent use a tool. It doesn't approve a specific post, message, purchase, deletion or settings change. Each such action asks separately in the inbox for its exact content and destination, and that confirmation is used once. If the content or recipient changes, it asks again. Reading and writing in the agent's own knowledge folder don't ask twice.
|
|
411
|
+
- **Existing agents.** Agents made before this version keep their current tools for now. Each one gets a single inbox card that lists its existing scope (the web research setting and the knowledge folder access it had). If you confirm, those become permissions. If you decline, the agent switches to no access. Either way the switch happens when the agent is idle, so a running turn finishes with its old scope. Without an answer nothing changes. The **Web research (web_search, webfetch)** switch no longer grants anything by itself.
|
|
412
|
+
- **Aside tabs.** An agent can read a tab you already have open in Aside only after you pick it. On the request card, press **Load open tabs** and choose the tab under **Choose the target tab**. The tab list is shown to you only; the agent receives just the tab you chose. Reading never clicks, types, moves or closes the tab. Clicking or typing needs a separate permission for the same tab, and posting or sending still needs its own confirmation. If the tab closes or moves to another site, the permission waits as **Target needs confirmation**. For its own work, an agent opens background tabs and closes them when it's done.
|
|
413
|
+
- **Code and commands.** Code execution and shell commands run confined. They can't reach the network, browser profiles, the gateway or its settings, and they see only their own work folder. Permission to run code doesn't replace file, web or browser permissions, and every file or web call inside it is checked again.
|
|
414
|
+
- **Where you can answer.** Answer tool requests on the web screen opened directly at the gateway address. The Android app can answer them too (see 4.3). An older app shows "Review this access request on the web".
|
|
415
|
+
- **Requirements.** This needs the gateway release that contains it and the engine it installs (patch manifest revision 6, engine `5.1.51-zgw.6`). An update to an engine without this support is refused before that engine is activated, and the gateway keeps the previous engine.
|
|
416
|
+
|
|
344
417
|
### 2.12 Agent rules
|
|
345
418
|
|
|
346
419
|
The gateway adds the common rules of the package and this installation's `~/.zigmini/rules.md` to every main and work session.
|
|
@@ -350,6 +423,29 @@ Replacing a common rule item: each item in the common rules file has a fixed id
|
|
|
350
423
|
- `zigmini rules`: lists the common items with number, id and first line.
|
|
351
424
|
- `zigmini rules check`: shows each `rules.md` item as "replaces common N", "new rule" or "unknown id" (exit code 1 when an unknown id exists).
|
|
352
425
|
|
|
426
|
+
### 2.13 Agent collaboration
|
|
427
|
+
|
|
428
|
+
Zig can hand a request to an agent, and an agent can ask another agent a question. Each request has its own record, and the reply comes back to the conversation that asked.
|
|
429
|
+
|
|
430
|
+
```zigmini-diagram The collaboration tools themselves need no **Tool access request**; every other tool still needs your grant. A blocked agent gets none of them.
|
|
431
|
+
{"version":1,"title":"A collaboration request","nodes":[{"id":"asker","label":"Asking conversation (Zig or an agent)"},{"id":"gateway","label":"Gateway request record"},{"id":"agent","label":"Receiving agent"},{"id":"inbox","label":"Inbox (your answer)"}],"edges":[{"from":"asker","to":"gateway"},{"from":"gateway","to":"agent"},{"from":"agent","to":"inbox"},{"from":"inbox","to":"agent"},{"from":"agent","to":"asker"}]}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- **Who may ask whom.** In the agent editor, **Collaboration** > **Agents it may ask** lists the agents this agent may send requests to. The permission goes one way, and the list is empty by default. Zig may ask any agent that isn't archived. An agent may only ask questions; it can't hand out tasks.
|
|
435
|
+
- **Cards in the chat.** Both conversations show a card: **Collaboration request sent** in the asking one and **Collaboration request received** in the other. The card shows **From**, **To**, the kind (**Task** or **Question**), the text, the state and, when it ends, the **Result** with its **Files**. An open page updates the card as the state and result change, without a reload.
|
|
436
|
+
- **States.** **Queued**, **Running**, **Waiting for another agent**, **Waiting for your answer**, **Preparing the result**, **Completed**, **Failed**, **Interrupted**, **Cancel requested**, **Cancelled**, **Delivery unconfirmed** and **Definition changed**. A request counts as **Completed** only when the agent submitted an explicit result; an agent that stops without one leaves the request **Interrupted**, and nothing runs it again.
|
|
437
|
+
- **Your turn comes first.** A request never interrupts a turn you started. It waits and runs after your turn ends.
|
|
438
|
+
- **Questions to you.** When an agent asks you a question while working on a request, only that request (and requests it started) waits as **Waiting for your answer**. Other requests keep running. Answer in the inbox; the request continues once with your answer. A **Tool access request** card the agent asks for while working on a request pauses only that request (and requests it started) as **Waiting for your answer** in the same way, and the request continues once after you answer. After the other agent replies to a request you started, a tool call that needs permission in the agent's next run also shows a **Tool access request** card, marked **Request from a collaboration you started**. Allowing it runs that call once, and the agent continues once.
|
|
439
|
+
- **Result files.** The agent saves files to `outputs/` and lists them in its result. The gateway seals a copy, so later edits in `outputs/` don't change the result. A file the agent received this way appears in its knowledge files as **Collaboration copy**; deleting it removes only that copy.
|
|
440
|
+
- **File read review.** When an agent reads a shared file, the gateway runs the security check on the part it asked for. If the check flags something, the read is held. A PDF page with no text layer can't be checked automatically, so it's held too. The inbox then shows a **File read review** card with **Reading agent**, **File**, **Requested part** and **Security check**. That last row lists only the finding kinds and their counts (for example, **Token / API key: 1**). The flagged values themselves never appear. For an unchecked page, the card asks you to look at it yourself first. **Allow this read** sends that part to the agent once. **Do not allow** sends nothing. If the gateway doesn't accept this browser as the owner, or can't be reached, the card says so. Nothing is processed in the first case, and in the second you should check the latest state. If the hold changed meanwhile, the card loads the latest state; check it and answer again.
|
|
441
|
+
- **Cancel and recheck.** **Cancel request** stops a request and the requests it started. A running request shows **Cancel requested** until the agent has stopped, then **Cancelled**. **Delivery unconfirmed** means the gateway can't tell whether the agent received the request (for example after a restart); it is never sent again on its own. **Recheck** reads the states again. Pressing **Stop** in an agent conversation while it works on a collaboration request cancels that request and the requests it started, just like **Cancel request**.
|
|
442
|
+
- **Ending without a result.** When a request ends as **Cancelled**, **Interrupted** (including a missing result) or **Definition changed**, the asking conversation still gets one final reply. That reply carries the state and the reason, but no text and no files. Nothing is delivered, and nothing runs again. If the asking request is itself finished or being cancelled, it gets no reply.
|
|
443
|
+
- **Asking again.** Only Zig can ask again, and only from the conversation that sent the first request. Zig sends a new request with `retryOf` set to the first request's id. That works when the first request ended in any final state except **Completed**: **Failed**, **Cancelled**, **Interrupted** or **Definition changed**. The same request sent again without `retryOf` is refused. Agents can't ask again this way.
|
|
444
|
+
- **Owner sign-in.** **Cancel request** and **Download** work only from the web page on this Mac or from an approved device. The page proves it is yours with the same owner check as agent tool permissions. If the check fails, the card says so, and nothing is cancelled or downloaded.
|
|
445
|
+
- **Deleting an agent.** An agent that still has open collaboration requests can't be deleted; cancel them or let them finish first.
|
|
446
|
+
- **Cost and limits.** Each request runs the receiving agent's model, so it costs like a normal turn. Agents may ask the same thing twice; the gateway doesn't merge similar requests. Results are what the agent submitted, not a checked answer.
|
|
447
|
+
- **Android.** The app doesn't show collaboration cards. Instead, its chat shows one system line for each collaboration message, such as "Sent a collaboration request: …", "Received the collaboration result: …" or "The collaboration request ended without a result: …". These lines don't send phone notifications.
|
|
448
|
+
|
|
353
449
|
## 3. Keyboard shortcuts
|
|
354
450
|
|
|
355
451
|
| Key | Where | What it does |
|
|
@@ -413,8 +509,11 @@ Open it from the inbox icon in the header. The top shows counts for "확인 필
|
|
|
413
509
|
- Pick an answer and send it.
|
|
414
510
|
- 삭제 removes the item after a confirm step. For a question still waiting, the button reads 질문 취소하고 삭제 (cancel question and delete).
|
|
415
511
|
- 원문 보기 jumps to the question card in chat.
|
|
512
|
+
- When a work waits on a required question and main sends it a new instruction, the work handles the new instruction at once. The question stays in the inbox, and the step that depends on it waits for your answer.
|
|
416
513
|
- While disconnected, the list is the last one received. Answers can be sent after reconnecting.
|
|
417
514
|
|
|
515
|
+
Tool access requests from agents (see Tool permissions in 2.11) appear here as **Tool access request** cards with **Allow this scope** and **Do not allow**. **Manage access** opens that permission, and **Revoke access** removes it. The app has no full agent editor; use the web for editing scopes. An app that doesn't support these cards shows "Review this access request on the web".
|
|
516
|
+
|
|
418
517
|
### 4.4 Work status screen
|
|
419
518
|
|
|
420
519
|
Tap the Zig face to open it. Slot states include running, waiting for input, waiting, waiting for subtasks, unresponsive, broken, completed, failed and cancelled.
|
|
@@ -458,6 +557,12 @@ Tap the Zig face to open it. Slot states include running, waiting for input, wai
|
|
|
458
557
|
|
|
459
558
|
The 매뉴얼 button on the gateway connection screen opens this document. See 2.10, Viewing the manual.
|
|
460
559
|
|
|
560
|
+
### 4.10 Mac files
|
|
561
|
+
|
|
562
|
+
Type `@` in the input field or tap **Mac files** to open **Files on this gateway Mac**. This sheet lists files on the gateway Mac, not on your phone. It's separate from the phone image picker, which works as before. Only a registered phone can open it. The formats, limits and rules are the same as on the web; see "Files on this Mac" in 2.3. **Save original** on a sent file saves the original to the phone.
|
|
563
|
+
|
|
564
|
+
If you revoke a phone's registration on the Mac, that phone immediately loses listing, selecting, status, download and send. Files already sent stay readable by their conversation until they're revoked.
|
|
565
|
+
|
|
461
566
|
## 5. Common
|
|
462
567
|
|
|
463
568
|
### 5.1 Giving work to Zig
|
|
@@ -489,6 +594,7 @@ The conversation may keep going while a question waits. If you can't tell whethe
|
|
|
489
594
|
| The gateway won't start and says consent is missing. | Run `zigmini consent` in the Mac terminal. |
|
|
490
595
|
| The app can't connect. | Check 외부 접속 in web settings. Without a choice, the phone can't connect. If the app shows auth refused, register the phone again. |
|
|
491
596
|
| Phone registration is locked. | Click 잠금 해제 on the request line in the web screen. |
|
|
597
|
+
| A Mac file shows **Mac access denied**. | macOS privacy protection is blocking the gateway from that location. Allow it yourself in System Settings > Privacy & Security, under Files and Folders or Full Disk Access. zigmini never changes System Settings. |
|
|
492
598
|
|
|
493
599
|
## 6. CLI
|
|
494
600
|
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import PDFKit
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
import CoreText
|
|
5
|
+
import Vision
|
|
6
|
+
import ImageIO
|
|
7
|
+
import CryptoKit
|
|
8
|
+
import Darwin
|
|
9
|
+
|
|
10
|
+
// SIZE_OK: T4's frozen ownership allows only this native module and its document-only entrypoint.
|
|
11
|
+
// Parsing never receives a pathname, URL, password, authority or caller-selected sandbox policy.
|
|
12
|
+
enum DocumentReadFailure: Error {
|
|
13
|
+
case failed(HelperFailureCode)
|
|
14
|
+
case isolation
|
|
15
|
+
}
|
|
16
|
+
enum DocumentRead {
|
|
17
|
+
static func digest(_ data: Data) -> String {
|
|
18
|
+
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
|
19
|
+
}
|
|
20
|
+
static func event(_ stage: String, page: Int = 0) {
|
|
21
|
+
let value = "{\"stage\":\"\(stage)\",\"page\":\(page)}\n"
|
|
22
|
+
FileHandle.standardError.write(Data(value.utf8))
|
|
23
|
+
}
|
|
24
|
+
static func recognizer() throws -> VNRecognizeTextRequest {
|
|
25
|
+
for revision in VNRecognizeTextRequest.supportedRevisions.sorted(by: >) {
|
|
26
|
+
let request = VNRecognizeTextRequest()
|
|
27
|
+
request.revision = revision
|
|
28
|
+
request.recognitionLevel = .accurate
|
|
29
|
+
let supported = try request.supportedRecognitionLanguages()
|
|
30
|
+
if supported.contains("ko-KR") && supported.contains("en-US") {
|
|
31
|
+
request.recognitionLanguages = ["ko-KR", "en-US"]
|
|
32
|
+
request.usesLanguageCorrection = true
|
|
33
|
+
return request
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw DocumentReadFailure.failed(.ocrFailed)
|
|
37
|
+
}
|
|
38
|
+
static func bitmap(width: Int, height: Int) throws -> CGContext {
|
|
39
|
+
guard width > 0, height > 0, width <= 12_000_000 / height,
|
|
40
|
+
let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8,
|
|
41
|
+
bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(),
|
|
42
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
|
|
43
|
+
throw DocumentReadFailure.failed(.processingLimit)
|
|
44
|
+
}
|
|
45
|
+
context.setFillColor(CGColor(gray: 1, alpha: 1))
|
|
46
|
+
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
|
47
|
+
return context
|
|
48
|
+
}
|
|
49
|
+
static func recognize(_ image: CGImage, with request: VNRecognizeTextRequest) throws -> String {
|
|
50
|
+
do {
|
|
51
|
+
try VNImageRequestHandler(cgImage: image, options: [:]).perform([request])
|
|
52
|
+
guard let observations = request.results else { throw DocumentReadFailure.failed(.ocrFailed) }
|
|
53
|
+
return observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
|
|
54
|
+
} catch {
|
|
55
|
+
throw DocumentReadFailure.failed(.ocrFailed)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Only fixed trusted data is touched here. No stdin access, document-dependent options or output.
|
|
59
|
+
// This initializes lazily loaded font/recognition resources before narrowing the process.
|
|
60
|
+
static func bootstrap() throws -> VNRecognizeTextRequest {
|
|
61
|
+
event("bootstrap-languages")
|
|
62
|
+
let request = try recognizer()
|
|
63
|
+
event("bootstrap-font")
|
|
64
|
+
let context = try bitmap(width: 1200, height: 400)
|
|
65
|
+
let font = CTFontCreateWithName("AppleSDGothicNeo-Regular" as CFString, 48, nil)
|
|
66
|
+
let line = CTLineCreateWithAttributedString(NSAttributedString(
|
|
67
|
+
string: "TRUSTED BOOTSTRAP 파란 하늘", attributes: [
|
|
68
|
+
NSAttributedString.Key(kCTFontAttributeName as String): font,
|
|
69
|
+
NSAttributedString.Key(kCTForegroundColorAttributeName as String): CGColor(gray: 0, alpha: 1)
|
|
70
|
+
]))
|
|
71
|
+
context.textPosition = CGPoint(x: 40, y: 180)
|
|
72
|
+
CTLineDraw(line, context)
|
|
73
|
+
guard let image = context.makeImage() else { throw DocumentReadFailure.failed(.ocrFailed) }
|
|
74
|
+
event("bootstrap-recognize")
|
|
75
|
+
_ = try recognize(image, with: request)
|
|
76
|
+
event("bootstrap-complete")
|
|
77
|
+
return request
|
|
78
|
+
}
|
|
79
|
+
static func raster(_ page: PDFPage, ocr: Bool) throws -> CGImage {
|
|
80
|
+
let bounds = page.bounds(for: .mediaBox)
|
|
81
|
+
guard bounds.width.isFinite, bounds.height.isFinite, bounds.width > 0, bounds.height > 0 else {
|
|
82
|
+
throw DocumentReadFailure.failed(.invalidPDF)
|
|
83
|
+
}
|
|
84
|
+
let rotated = abs(page.rotation % 180) == 90
|
|
85
|
+
let width = rotated ? bounds.height : bounds.width
|
|
86
|
+
let height = rotated ? bounds.width : bounds.height
|
|
87
|
+
let scale = ocr ? 300.0 / 72.0 : min(1, 1568 / max(width, height))
|
|
88
|
+
let w = ceil(width * scale), h = ceil(height * scale)
|
|
89
|
+
guard w.isFinite, h.isFinite, w >= 1, h >= 1, w * h <= 12_000_000,
|
|
90
|
+
w <= 12_000_000, h <= 12_000_000 else { throw DocumentReadFailure.failed(.processingLimit) }
|
|
91
|
+
let context = try bitmap(width: Int(w), height: Int(h))
|
|
92
|
+
guard let reference = page.pageRef else { throw DocumentReadFailure.failed(.invalidPDF) }
|
|
93
|
+
let destination = CGRect(x: 0, y: 0, width: w, height: h)
|
|
94
|
+
context.concatenate(reference.getDrawingTransform(.mediaBox, rect: destination, rotate: 0, preserveAspectRatio: true))
|
|
95
|
+
context.drawPDFPage(reference)
|
|
96
|
+
guard let image = context.makeImage() else { throw DocumentReadFailure.failed(.processingLimit) }
|
|
97
|
+
return image
|
|
98
|
+
}
|
|
99
|
+
static func jpeg(_ image: CGImage) throws -> Data {
|
|
100
|
+
let result = NSMutableData()
|
|
101
|
+
guard let destination = CGImageDestinationCreateWithData(result, "public.jpeg" as CFString, 1, nil) else {
|
|
102
|
+
throw DocumentReadFailure.failed(.processingLimit)
|
|
103
|
+
}
|
|
104
|
+
CGImageDestinationAddImage(destination, image,
|
|
105
|
+
[kCGImageDestinationLossyCompressionQuality: 0.8] as CFDictionary)
|
|
106
|
+
guard CGImageDestinationFinalize(destination), result.length <= HelperLimits.imageBytes else {
|
|
107
|
+
throw DocumentReadFailure.failed(.processingLimit)
|
|
108
|
+
}
|
|
109
|
+
return result as Data
|
|
110
|
+
}
|
|
111
|
+
static func decode(_ bytes: Data, encoding: DocumentEncoding?) throws -> String {
|
|
112
|
+
let values = [UInt8](bytes.prefix(3))
|
|
113
|
+
var bom: DocumentEncoding?, skip = 0
|
|
114
|
+
if values.starts(with: [0xef, 0xbb, 0xbf]) { bom = .utf8; skip = 3 }
|
|
115
|
+
else if values.starts(with: [0xff, 0xfe]) { bom = .utf16le; skip = 2 }
|
|
116
|
+
else if values.starts(with: [0xfe, 0xff]) { bom = .utf16be; skip = 2 }
|
|
117
|
+
if let bom, let encoding, bom != encoding { throw DocumentReadFailure.failed(.unsupportedEncoding) }
|
|
118
|
+
let selected = encoding ?? bom ?? .utf8
|
|
119
|
+
let native: String.Encoding
|
|
120
|
+
switch selected {
|
|
121
|
+
case .utf8: native = .utf8
|
|
122
|
+
case .utf16le: native = .utf16LittleEndian
|
|
123
|
+
case .utf16be: native = .utf16BigEndian
|
|
124
|
+
case .eucKR:
|
|
125
|
+
native = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(
|
|
126
|
+
CFStringEncoding(CFStringEncodings.EUC_KR.rawValue)))
|
|
127
|
+
case .cp949:
|
|
128
|
+
native = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(
|
|
129
|
+
CFStringEncoding(CFStringEncodings.dosKorean.rawValue)))
|
|
130
|
+
}
|
|
131
|
+
let data = bytes.dropFirst(skip)
|
|
132
|
+
guard let text = String(data: data, encoding: native),
|
|
133
|
+
!text.contains("\0"), let roundtrip = text.data(using: native, allowLossyConversion: false),
|
|
134
|
+
roundtrip == data else {
|
|
135
|
+
throw DocumentReadFailure.failed(encoding == nil && bom == nil ? .encodingRequired : .unsupportedEncoding)
|
|
136
|
+
}
|
|
137
|
+
return text.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
|
|
138
|
+
}
|
|
139
|
+
// Vision also reads a page's selectable text. Only whole lines the native text lacks are new content.
|
|
140
|
+
static func addedLines(_ recognized: String, beyond native: String) -> String {
|
|
141
|
+
let squash = { (line: Substring) in line.filter { !$0.isWhitespace }.lowercased() }
|
|
142
|
+
let known = Set(native.split(separator: "\n").map(squash))
|
|
143
|
+
return recognized.split(separator: "\n").filter { !known.contains(squash($0)) }.joined(separator: "\n")
|
|
144
|
+
}
|
|
145
|
+
static func handle(_ request: DocumentRequest, recognizer: VNRecognizeTextRequest?,
|
|
146
|
+
emit: (HelperFrame) throws -> Void) throws {
|
|
147
|
+
let original = try request.originalBytes()
|
|
148
|
+
if request.options.format != .pdf {
|
|
149
|
+
guard request.options.format != .html,
|
|
150
|
+
request.operation == .text || request.operation == .inspect else {
|
|
151
|
+
throw HelperProtocolError.invalidOperation
|
|
152
|
+
}
|
|
153
|
+
let text = try decode(original, encoding: request.options.encoding)
|
|
154
|
+
if request.operation == .inspect {
|
|
155
|
+
try emit(.documentComplete(DocumentComplete(requestId: request.requestId,
|
|
156
|
+
originalSha256: digest(original), representationSha256: digest(Data(text.utf8)),
|
|
157
|
+
coverage: DocumentCoverage(totalPages: 1, pages: [PageCoverage(page: 1, representation: .nativeText)]),
|
|
158
|
+
content: [])))
|
|
159
|
+
} else {
|
|
160
|
+
try emit(PreparedDocumentText(pages: [PreparedTextPage(page: 1, representation: .nativeText, text: text)]).frame(for: request))
|
|
161
|
+
}
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
guard original.starts(with: Data("%PDF-".utf8)), let pdf = PDFDocument(data: original) else {
|
|
165
|
+
throw DocumentReadFailure.failed(.invalidPDF)
|
|
166
|
+
}
|
|
167
|
+
guard !pdf.isLocked else { throw DocumentReadFailure.failed(.passwordRequired) }
|
|
168
|
+
guard pdf.pageCount > 0 else { throw DocumentReadFailure.failed(.invalidPDF) }
|
|
169
|
+
guard pdf.pageCount <= 300 else { throw DocumentReadFailure.failed(.processingLimit) }
|
|
170
|
+
var prepared: [PreparedTextPage] = [], coverage: [PageCoverage] = [], images: [HelperContent] = []
|
|
171
|
+
var bytes = 0
|
|
172
|
+
let first = request.options.firstPage ?? 1
|
|
173
|
+
let end = min(first + (request.options.pageLimit ?? 2), pdf.pageCount + 1)
|
|
174
|
+
// A text or OCR call without a range reads the whole document. Pages always use a range.
|
|
175
|
+
let ranged = request.operation == .pages || (request.operation != .inspect
|
|
176
|
+
&& (request.options.firstPage != nil || request.options.pageLimit != nil))
|
|
177
|
+
if ranged && first >= end { throw HelperProtocolError.invalidBounds }
|
|
178
|
+
for index in 0..<pdf.pageCount {
|
|
179
|
+
if ranged && !(first..<end).contains(index + 1) { continue }
|
|
180
|
+
try autoreleasepool {
|
|
181
|
+
event("page-start", page: index + 1)
|
|
182
|
+
defer { event("page-end", page: index + 1) }
|
|
183
|
+
guard let page = pdf.page(at: index) else { throw DocumentReadFailure.failed(.invalidPDF) }
|
|
184
|
+
let native = (page.string ?? "").replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
|
|
185
|
+
guard !native.contains("\0") else { throw DocumentReadFailure.failed(.invalidPDF) }
|
|
186
|
+
if request.operation == .pages {
|
|
187
|
+
let image = try raster(page, ocr: false)
|
|
188
|
+
let data = try jpeg(image)
|
|
189
|
+
images.append(.image(HelperImage(data: data.base64EncodedString(), mimeType: .jpeg)))
|
|
190
|
+
coverage.append(PageCoverage(page: index + 1, representation: .renderedImage))
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
var text = native, representation: PageRepresentation = .nativeText
|
|
194
|
+
if request.operation == .ocr {
|
|
195
|
+
guard let recognizer else { throw DocumentReadFailure.failed(.ocrFailed) }
|
|
196
|
+
event("ocr", page: index + 1)
|
|
197
|
+
let recognized = try recognize(raster(page, ocr: true), with: recognizer)
|
|
198
|
+
text = "[Page \(index + 1) native]\n\(native)\n[Page \(index + 1) OCR]\n\(addedLines(recognized, beyond: native))\n"
|
|
199
|
+
representation = .ocrText
|
|
200
|
+
}
|
|
201
|
+
// Empty native text is not a verified blank page. It remains native-text.
|
|
202
|
+
if request.operation != .ocr && !native.isEmpty { text = "[Page \(index + 1) native]\n\(native)\n" }
|
|
203
|
+
bytes += text.utf8.count
|
|
204
|
+
guard bytes <= HelperLimits.preparedTextBytes else { throw DocumentReadFailure.failed(.processingLimit) }
|
|
205
|
+
prepared.append(PreparedTextPage(page: index + 1, representation: representation, text: text))
|
|
206
|
+
coverage.append(PageCoverage(page: index + 1,
|
|
207
|
+
representation: request.operation == .inspect && native.isEmpty ? .renderedImage : representation))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
switch request.operation {
|
|
211
|
+
case .text, .ocr:
|
|
212
|
+
try emit(PreparedDocumentText(pages: prepared, totalPages: pdf.pageCount).frame(for: request))
|
|
213
|
+
case .inspect:
|
|
214
|
+
let representation = try PreparedDocumentText(pages: prepared)
|
|
215
|
+
try emit(.documentComplete(DocumentComplete(requestId: request.requestId, originalSha256: digest(original),
|
|
216
|
+
representationSha256: representation.sha256,
|
|
217
|
+
coverage: DocumentCoverage(totalPages: pdf.pageCount, pages: coverage), content: [])))
|
|
218
|
+
case .pages:
|
|
219
|
+
let imageData = images.compactMap { item -> Data? in
|
|
220
|
+
if case .image(let image) = item { return Data(base64Encoded: image.data) }
|
|
221
|
+
return nil
|
|
222
|
+
}.reduce(into: Data()) { $0.append($1) }
|
|
223
|
+
try emit(.documentComplete(DocumentComplete(requestId: request.requestId, originalSha256: digest(original),
|
|
224
|
+
representationSha256: digest(imageData), coverage: DocumentCoverage(totalPages: pdf.pageCount, pages: coverage),
|
|
225
|
+
content: images)))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#if DOCUMENT_READER_MAIN
|
|
231
|
+
// Custom kernel policy is installed after trusted framework bootstrap, strictly before readRequest.
|
|
232
|
+
// No App Sandbox bundle, protected container, external files, networking or native output directory.
|
|
233
|
+
@_silgen_name("sandbox_init")
|
|
234
|
+
private func documentSandboxInit(_ profile: UnsafePointer<CChar>, _ flags: UInt64,
|
|
235
|
+
_ error: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>) -> Int32
|
|
236
|
+
@_silgen_name("sandbox_free_error")
|
|
237
|
+
private func documentSandboxFreeError(_ error: UnsafeMutablePointer<CChar>?)
|
|
238
|
+
|
|
239
|
+
@main struct DocumentReaderEntrypoint {
|
|
240
|
+
// Vision's precompiled runtime reopens its per-executable model cache and the Neural Engine
|
|
241
|
+
// client after bootstrap when a page size differs. Those two grants exist only for OCR.
|
|
242
|
+
static func ocrRules() throws -> String {
|
|
243
|
+
guard let entry = getpwuid(getuid()), let home = entry.pointee.pw_dir else { throw DocumentReadFailure.isolation }
|
|
244
|
+
let quoted = URL(fileURLWithPath: String(cString: home)).appendingPathComponent("Library/Caches")
|
|
245
|
+
.appendingPathComponent(ProcessInfo.processInfo.processName)
|
|
246
|
+
.appendingPathComponent("com.apple.e5rt.e5bundlecache").resolvingSymlinksInPath().path
|
|
247
|
+
.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
|
|
248
|
+
return "(allow file-read* (subpath \"\(quoted)\"))\n(allow iokit-open (iokit-user-client-class \"H11ANEInDirectPathClient\"))\n"
|
|
249
|
+
}
|
|
250
|
+
static func isolate(ocr: Bool) throws {
|
|
251
|
+
let profile = """
|
|
252
|
+
(version 1)
|
|
253
|
+
(deny default)
|
|
254
|
+
(allow signal (target self))
|
|
255
|
+
(allow sysctl-read)
|
|
256
|
+
(allow file-read-metadata)
|
|
257
|
+
(allow file-read* (subpath "/System") (subpath "/usr/lib") (subpath "/usr/share")
|
|
258
|
+
(subpath "/Library/Fonts") (literal "/dev/null") (literal "/dev/random") (literal "/dev/urandom"))
|
|
259
|
+
(allow file-map-executable (subpath "/System") (subpath "/usr/lib"))
|
|
260
|
+
(allow mach-lookup (global-name "com.apple.system.logger") (global-name "com.apple.logd")
|
|
261
|
+
(global-name "com.apple.FontObjectsServer") (global-name "com.apple.fonts")
|
|
262
|
+
(global-name "com.apple.cfprefsd.agent") (global-name "com.apple.cfprefsd.daemon")
|
|
263
|
+
(global-name "com.apple.securityd.xpc") (global-name "com.apple.trustd.agent"))
|
|
264
|
+
|
|
265
|
+
""" + (ocr ? try ocrRules() : "")
|
|
266
|
+
var error: UnsafeMutablePointer<CChar>?
|
|
267
|
+
let result = profile.withCString { documentSandboxInit($0, 0, &error) }
|
|
268
|
+
defer { documentSandboxFreeError(error) }
|
|
269
|
+
guard result == 0 else { throw DocumentReadFailure.isolation }
|
|
270
|
+
DocumentRead.event("isolated")
|
|
271
|
+
}
|
|
272
|
+
// Package gate. Fixed synthetic data only: it reads no stdin, user path or document.
|
|
273
|
+
static func selfTest() -> Never {
|
|
274
|
+
var korean = false, english = false
|
|
275
|
+
for revision in VNRecognizeTextRequest.supportedRevisions {
|
|
276
|
+
let request = VNRecognizeTextRequest()
|
|
277
|
+
request.revision = revision
|
|
278
|
+
request.recognitionLevel = .accurate
|
|
279
|
+
guard let supported = try? request.supportedRecognitionLanguages() else { continue }
|
|
280
|
+
korean = korean || supported.contains("ko-KR")
|
|
281
|
+
english = english || supported.contains("en-US")
|
|
282
|
+
}
|
|
283
|
+
let recognizer = try? DocumentRead.bootstrap()
|
|
284
|
+
var sandbox = false
|
|
285
|
+
if (try? isolate(ocr: recognizer != nil)) != nil {
|
|
286
|
+
let descriptor = open("/private/etc/hosts", O_RDONLY)
|
|
287
|
+
sandbox = descriptor < 0 && (errno == EPERM || errno == EACCES)
|
|
288
|
+
if descriptor >= 0 { close(descriptor) }
|
|
289
|
+
}
|
|
290
|
+
let bytes = NSMutableData()
|
|
291
|
+
var box = CGRect(x: 0, y: 0, width: 72, height: 72)
|
|
292
|
+
var pdfkit = false
|
|
293
|
+
if let consumer = CGDataConsumer(data: bytes), let context = CGContext(consumer: consumer, mediaBox: &box, nil) {
|
|
294
|
+
context.beginPDFPage(nil)
|
|
295
|
+
context.endPDFPage()
|
|
296
|
+
context.closePDF()
|
|
297
|
+
pdfkit = PDFDocument(data: bytes as Data)?.pageCount == 1
|
|
298
|
+
}
|
|
299
|
+
var recognized = false
|
|
300
|
+
if let recognizer, let image = (try? DocumentRead.bitmap(width: 200, height: 80))?.makeImage() {
|
|
301
|
+
recognized = (try? DocumentRead.recognize(image, with: recognizer)) != nil
|
|
302
|
+
}
|
|
303
|
+
let ok = pdfkit && sandbox && recognized && korean && english
|
|
304
|
+
print("{\"type\":\"self-test\",\"version\":1,\"pdfkit\":\(pdfkit),\"ocr\":{\"revision\":\(recognizer?.revision ?? 0),\"recognized\":\(recognized),\"ko-KR\":\(korean),\"en-US\":\(english)},\"sandbox\":\(sandbox),\"ok\":\(ok)}")
|
|
305
|
+
exit(ok ? 0 : 1)
|
|
306
|
+
}
|
|
307
|
+
static func main() {
|
|
308
|
+
do {
|
|
309
|
+
let arguments = Array(CommandLine.arguments.dropFirst())
|
|
310
|
+
if arguments == ["--self-test"] {
|
|
311
|
+
alarm(60)
|
|
312
|
+
selfTest()
|
|
313
|
+
}
|
|
314
|
+
guard arguments.isEmpty || arguments == ["--ocr"] || arguments == ["--isolation-probe"]
|
|
315
|
+
|| arguments == ["--isolation-probe", "--ocr"] else { exit(64) }
|
|
316
|
+
alarm(60)
|
|
317
|
+
// The trusted supervisor selects this fixed nonpath mode, not document content.
|
|
318
|
+
// Vision routes do not recognize even synthetic bootstrap images.
|
|
319
|
+
let ocr = arguments.contains("--ocr")
|
|
320
|
+
let recognizer = ocr ? try DocumentRead.bootstrap() : nil
|
|
321
|
+
try isolate(ocr: ocr)
|
|
322
|
+
alarm(0)
|
|
323
|
+
if let recognizer {
|
|
324
|
+
FileHandle.standardError.write(Data(
|
|
325
|
+
"{\"stage\":\"ocr-config\",\"revision\":\(recognizer.revision),\"languages\":[\"ko-KR\",\"en-US\"]}\n".utf8))
|
|
326
|
+
}
|
|
327
|
+
#if DOCUMENT_READER_TESTING
|
|
328
|
+
if CommandLine.arguments.dropFirst().first == "--isolation-probe" {
|
|
329
|
+
guard let path = ProcessInfo.processInfo.environment["DOCUMENT_OWNED_PROBE"] else { exit(70) }
|
|
330
|
+
let fd = open(path, O_RDONLY | O_NOFOLLOW)
|
|
331
|
+
let fileDenied = fd < 0 && (errno == EPERM || errno == EACCES)
|
|
332
|
+
if fd >= 0 { close(fd) }
|
|
333
|
+
let descriptor = socket(AF_INET, SOCK_STREAM, 0)
|
|
334
|
+
var networkDenied = descriptor < 0 && (errno == EPERM || errno == EACCES)
|
|
335
|
+
if descriptor >= 0 {
|
|
336
|
+
var address = sockaddr_in()
|
|
337
|
+
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
338
|
+
address.sin_family = sa_family_t(AF_INET); address.sin_port = UInt16(9).bigEndian
|
|
339
|
+
address.sin_addr.s_addr = inet_addr("127.0.0.1")
|
|
340
|
+
let result = withUnsafePointer(to: &address) {
|
|
341
|
+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
342
|
+
connect(descriptor, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
networkDenied = result < 0 && (errno == EPERM || errno == EACCES)
|
|
346
|
+
close(descriptor)
|
|
347
|
+
}
|
|
348
|
+
print("{\"fileDenied\":\(fileDenied),\"networkDenied\":\(networkDenied)}")
|
|
349
|
+
exit(fileDenied && networkDenied ? 0 : 71)
|
|
350
|
+
}
|
|
351
|
+
#endif
|
|
352
|
+
let request = try HelperCodec.readRequest(from: .standardInput, privilege: .document)
|
|
353
|
+
guard case .document(let document) = request else { throw HelperProtocolError.wrongPrivilege }
|
|
354
|
+
let writer = try HelperFrameWriter(request: request, output: .standardOutput)
|
|
355
|
+
do {
|
|
356
|
+
try DocumentRead.handle(document, recognizer: recognizer, emit: writer.emit)
|
|
357
|
+
try writer.finish()
|
|
358
|
+
} catch DocumentReadFailure.failed(let code) {
|
|
359
|
+
try writer.emit(.error(HelperFailure(requestId: document.requestId, code: code)))
|
|
360
|
+
try writer.finish()
|
|
361
|
+
exit(2)
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// Boundary diagnostics contain only typed stage names, never input, paths or OS identity.
|
|
365
|
+
DocumentRead.event(error is DocumentReadFailure ? "native-initialization-failed" : "invalid-request")
|
|
366
|
+
exit(70)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
#endif
|