zigmini 1.0.0-beta.2 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +36 -3
- package/README.md +36 -3
- package/assets/agent/gateway-request-rules.md +1 -1
- package/assets/agent/gateway-rules.md +6 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/assets/document-reader/document-reader +0 -0
- package/assets/document-reader/host-selector +0 -0
- package/assets/document-reader/manifest.json +27 -0
- package/docs/manual.ko.md +120 -11
- package/docs/manual.md +123 -12
- package/host/document-reader/DocumentRead.swift +370 -0
- package/host/document-reader/HostSelector.swift +192 -0
- package/host/document-reader/Protocol.swift +647 -0
- package/host/document-reader/main.swift +22 -0
- package/host/omo-bridge/agent-aside-tool.ts +119 -0
- package/host/omo-bridge/agent-aside.ts +522 -0
- package/host/omo-bridge/agent-collaboration-files.ts +282 -0
- package/host/omo-bridge/agent-collaboration.ts +385 -0
- package/host/omo-bridge/agent-control.ts +400 -28
- package/host/omo-bridge/agent-execution.sb +42 -0
- package/host/omo-bridge/agent-execution.ts +158 -0
- package/host/omo-bridge/agent-tool-adapters.ts +388 -0
- package/host/omo-bridge/agent-tool-permissions.ts +96 -0
- package/host/omo-bridge/aside-tabs.ts +41 -0
- package/host/omo-bridge/attachment-read.ts +100 -0
- package/host/omo-bridge/bridge.ts +10 -0
- package/host/omo-bridge/managed-call.ts +46 -0
- package/host/omo-bridge/task-control.ts +39 -5
- package/host/omo-check/patches/attachments-293/README.md +138 -0
- package/host/omo-check/patches/attachments-293/document-content.js +234 -0
- package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
- package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
- package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
- package/host/omo-check/patches/omo-5.1.51-chatgpt-terra.patch +21 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +44 -6
- package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
- package/package.json +5 -3
- package/src/agent/api.ts +345 -20
- package/src/agent/artifacts.ts +196 -0
- package/src/agent/aside-target-routes.ts +369 -0
- package/src/agent/aside-targets.ts +215 -0
- package/src/agent/collaboration-admission.ts +47 -0
- package/src/agent/collaboration-read.ts +117 -0
- package/src/agent/collaboration-session.ts +196 -0
- package/src/agent/collaboration-types.ts +258 -0
- package/src/agent/collaboration.ts +399 -0
- package/src/agent/definition.ts +65 -3
- package/src/agent/fields.ts +9 -3
- package/src/agent/file-core.ts +325 -0
- package/src/agent/files.ts +87 -8
- package/src/agent/list.ts +3 -3
- package/src/agent/managed-call.ts +144 -0
- package/src/agent/tool-catalog.ts +184 -0
- package/src/agent/tool-delivery.ts +359 -0
- package/src/agent/tool-permission-types.ts +81 -0
- package/src/agent/tool-permissions.ts +1366 -0
- package/src/attachments/api.ts +161 -0
- package/src/attachments/document-reader.ts +325 -0
- package/src/attachments/host-selector.ts +410 -0
- package/src/attachments/managed-binding.ts +83 -0
- package/src/attachments/prepare.ts +214 -0
- package/src/attachments/selector-port.ts +28 -0
- package/src/attachments/service.ts +729 -0
- package/src/attachments/store.ts +698 -0
- package/src/attachments/types.ts +123 -0
- package/src/cli/doctor.ts +30 -10
- package/src/decisions.ts +109 -10
- package/src/engine/apply-patch.ts +21 -0
- package/src/engine/compat.json +2 -1
- package/src/engine/install.ts +45 -4
- package/src/engine/sandbox-cell.ts +2 -0
- package/src/gc.ts +33 -225
- package/src/housekeeping/api.ts +163 -0
- package/src/housekeeping/audit.ts +61 -0
- package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
- package/src/housekeeping/collectors/agent-drafts.ts +43 -0
- package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
- package/src/housekeeping/collectors/agent-trash.ts +28 -0
- package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
- package/src/housekeeping/collectors/db-backup.ts +25 -0
- package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
- package/src/housekeeping/collectors/docker-image.ts +135 -0
- package/src/housekeeping/collectors/omo-version.ts +32 -0
- package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
- package/src/housekeeping/collectors/reconcile-work.ts +55 -0
- package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
- package/src/housekeeping/collectors/shared.ts +149 -0
- package/src/housekeeping/collectors/temp-dir.ts +28 -0
- package/src/housekeeping/collectors/update-file.ts +25 -0
- package/src/housekeeping/collectors/worktree.ts +193 -0
- package/src/housekeeping/engine.ts +125 -0
- package/src/housekeeping/reconcile/work.ts +130 -0
- package/src/housekeeping/registry.ts +63 -0
- package/src/housekeeping/trash.ts +59 -0
- package/src/housekeeping/types.ts +31 -0
- package/src/hub.ts +1095 -41
- package/src/main.ts +230 -31
- package/src/message-references.ts +14 -5
- package/src/profile-prompt.ts +13 -1
- package/src/profile.ts +13 -5
- package/src/protocol.ts +41 -6
- package/src/security-allow.ts +79 -2
- package/src/security-check.ts +1 -1
- package/src/store.ts +1946 -23
- package/src/test-crash.ts +20 -0
- package/src/usage.ts +18 -7
- package/src/work-watch.ts +55 -7
- package/src/work.ts +192 -3
- package/web/DESIGN.md +35 -1
- package/web/agents.d.ts +189 -12
- package/web/agents.js +898 -39
- package/web/app.js +2634 -210
- package/web/attachments.d.ts +91 -0
- package/web/attachments.js +422 -0
- package/web/collaboration.d.ts +87 -0
- package/web/collaboration.js +290 -0
- package/web/decisions.d.ts +39 -2
- package/web/decisions.js +354 -2
- package/web/gc.d.ts +26 -5
- package/web/gc.js +113 -13
- package/web/i18n/agents.d.ts +4 -0
- package/web/i18n/agents.js +200 -8
- package/web/i18n/attachments.js +78 -0
- package/web/i18n/decisions.d.ts +4 -0
- package/web/i18n/decisions.js +68 -0
- package/web/i18n/gc.d.ts +4 -0
- package/web/i18n/gc.js +184 -2
- package/web/i18n/main.js +80 -8
- package/web/i18n/tour.d.ts +4 -0
- package/web/i18n/tour.js +37 -0
- package/web/index.html +20 -3
- package/web/profile.d.ts +2 -2
- package/web/profile.js +16 -10
- package/web/style.css +342 -21
- package/web/tooltip.d.ts +2 -0
- package/web/tooltip.js +12 -0
- package/web/tour.d.ts +57 -0
- package/web/tour.js +450 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { AttachmentEncoding } from "./types.ts";
|
|
3
|
+
import type { NativeDocumentBuild, DocumentReadContext, NativeTextCursor } from "./document-reader.ts";
|
|
4
|
+
|
|
5
|
+
export type DocumentPreparationCode = "encoding_required" | "unsupported_encoding" | "invalid_text"
|
|
6
|
+
| "empty_file" | "processing_limit" | "invalid_document_options" | "invalid_cursor" | "native_reader_unavailable";
|
|
7
|
+
export class DocumentPreparationError extends Error {
|
|
8
|
+
constructor(readonly code: DocumentPreparationCode, readonly limit?: "original_bytes" | "prepared_bytes",
|
|
9
|
+
readonly encoding?: AttachmentEncoding) { super(code); this.name = "DocumentPreparationError"; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TextPreparationOptions {
|
|
13
|
+
readonly format: "txt" | "md" | "html";
|
|
14
|
+
readonly encoding?: AttachmentEncoding;
|
|
15
|
+
/** Trusted build manifest identities, not permissions or model-supplied paths. */
|
|
16
|
+
readonly helperSourceSha256: string;
|
|
17
|
+
readonly helperBuildSha256: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PreparedText {
|
|
20
|
+
readonly format: TextPreparationOptions["format"];
|
|
21
|
+
readonly encoding: AttachmentEncoding;
|
|
22
|
+
readonly originalSha256: string;
|
|
23
|
+
readonly representationSha256: string;
|
|
24
|
+
readonly representationKey: string;
|
|
25
|
+
readonly optionsSha256: string;
|
|
26
|
+
readonly helperSourceSha256: string;
|
|
27
|
+
readonly helperBuildSha256: string;
|
|
28
|
+
readonly text: string;
|
|
29
|
+
readonly bytes: number;
|
|
30
|
+
readonly scalars: number;
|
|
31
|
+
readonly sourceEscaped?: string;
|
|
32
|
+
readonly sourceEscapedSha256?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface TextCursor {
|
|
35
|
+
readonly originalSha256: string;
|
|
36
|
+
readonly representationSha256: string;
|
|
37
|
+
readonly representationKey: string;
|
|
38
|
+
readonly offset: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PreparedTextRead {
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly originalSha256: string;
|
|
43
|
+
readonly representationSha256: string;
|
|
44
|
+
readonly range: { readonly start: number; readonly end: number; readonly totalScalars: number; readonly totalBytes: number };
|
|
45
|
+
readonly nextCursor?: TextCursor;
|
|
46
|
+
}
|
|
47
|
+
const encodings: readonly AttachmentEncoding[] = ["utf-8", "utf-16le", "utf-16be", "euc-kr", "cp949"];
|
|
48
|
+
const sha = (input: string | Uint8Array): string => createHash("sha256").update(input).digest("hex");
|
|
49
|
+
const hashPattern = /^[0-9a-f]{64}$/;
|
|
50
|
+
const normalize = (text: string): string => text.replace(/\r\n?/g, "\n");
|
|
51
|
+
|
|
52
|
+
function parseOptions(input: unknown): TextPreparationOptions {
|
|
53
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new DocumentPreparationError("invalid_document_options");
|
|
54
|
+
const keys = ["format", "encoding", "helperSourceSha256", "helperBuildSha256"];
|
|
55
|
+
if (Object.keys(input).some(key => !keys.includes(key))) throw new DocumentPreparationError("invalid_document_options");
|
|
56
|
+
if (!("format" in input) || (input.format !== "txt" && input.format !== "md" && input.format !== "html")
|
|
57
|
+
|| !("helperSourceSha256" in input) || typeof input.helperSourceSha256 !== "string" || !hashPattern.test(input.helperSourceSha256)
|
|
58
|
+
|| !("helperBuildSha256" in input) || typeof input.helperBuildSha256 !== "string" || !hashPattern.test(input.helperBuildSha256)) {
|
|
59
|
+
throw new DocumentPreparationError("invalid_document_options");
|
|
60
|
+
}
|
|
61
|
+
if ("encoding" in input && input.encoding !== undefined) {
|
|
62
|
+
const encoding = encodings.find(value => value === input.encoding);
|
|
63
|
+
if (!encoding) throw new DocumentPreparationError("unsupported_encoding");
|
|
64
|
+
return { format: input.format, helperSourceSha256: input.helperSourceSha256, helperBuildSha256: input.helperBuildSha256, encoding };
|
|
65
|
+
}
|
|
66
|
+
return { format: input.format, helperSourceSha256: input.helperSourceSha256, helperBuildSha256: input.helperBuildSha256 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** This is tokenization only: there is no browser, DOM, script evaluation or resource loader. */
|
|
70
|
+
function stripActive(source: string): string {
|
|
71
|
+
return new HTMLRewriter().on("script,style,template", { element(element) { element.remove(); } }).transform(source);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function htmlCharset(original: Uint8Array): AttachmentEncoding | undefined {
|
|
75
|
+
// Metadata syntax is ASCII. Latin-1 is used only for tokenization, never as a content decoder.
|
|
76
|
+
const syntax = stripActive(new TextDecoder("latin1").decode(original));
|
|
77
|
+
let charset: string | undefined;
|
|
78
|
+
new HTMLRewriter().on("meta", {
|
|
79
|
+
element(element) {
|
|
80
|
+
if (charset !== undefined) return;
|
|
81
|
+
const declared = element.getAttribute("charset");
|
|
82
|
+
if (declared !== null) { charset = declared.trim().toLowerCase(); return; }
|
|
83
|
+
if (element.getAttribute("http-equiv")?.toLowerCase() === "content-type") {
|
|
84
|
+
charset = /charset\s*=\s*["']?([a-z0-9-]+)/i.exec(element.getAttribute("content") ?? "")?.[1]?.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}).transform(syntax);
|
|
88
|
+
if (charset === undefined) return undefined;
|
|
89
|
+
const encoding = encodings.find(value => value === charset);
|
|
90
|
+
if (!encoding) throw new DocumentPreparationError("unsupported_encoding");
|
|
91
|
+
return encoding;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface NativeTextPreparation {
|
|
95
|
+
readonly build: NativeDocumentBuild;
|
|
96
|
+
readonly context: DocumentReadContext;
|
|
97
|
+
}
|
|
98
|
+
async function decode(original: Uint8Array, options: TextPreparationOptions,
|
|
99
|
+
native?: NativeTextPreparation): Promise<{ text: string; encoding: AttachmentEncoding }> {
|
|
100
|
+
let bom: AttachmentEncoding | undefined, skip = 0;
|
|
101
|
+
if (original[0] === 0xef && original[1] === 0xbb && original[2] === 0xbf) { bom = "utf-8"; skip = 3; }
|
|
102
|
+
else if (original[0] === 0xff && original[1] === 0xfe) { bom = "utf-16le"; skip = 2; }
|
|
103
|
+
else if (original[0] === 0xfe && original[1] === 0xff) { bom = "utf-16be"; skip = 2; }
|
|
104
|
+
if (bom && options.encoding && bom !== options.encoding) throw new DocumentPreparationError("unsupported_encoding");
|
|
105
|
+
const declared = !bom && !options.encoding && options.format === "html" ? htmlCharset(original) : undefined;
|
|
106
|
+
const encoding = options.encoding ?? bom ?? declared ?? "utf-8";
|
|
107
|
+
switch (encoding) {
|
|
108
|
+
case "euc-kr": case "cp949": {
|
|
109
|
+
// Do not substitute ICU/WHATWG EUC-KR (Windows-949) for strict Foundation decoding.
|
|
110
|
+
if (!native || native.build.sourceSha256 !== options.helperSourceSha256
|
|
111
|
+
|| native.build.buildSha256 !== options.helperBuildSha256) {
|
|
112
|
+
throw new DocumentPreparationError("native_reader_unavailable", undefined, encoding);
|
|
113
|
+
}
|
|
114
|
+
const { readNativeDocument } = await import("./document-reader.ts");
|
|
115
|
+
const parts: string[] = [];
|
|
116
|
+
let cursor: NativeTextCursor | undefined, offset = 0, representation: string | undefined, total = 0;
|
|
117
|
+
do {
|
|
118
|
+
const result = await readNativeDocument(native.build, { original, operation: "document.text",
|
|
119
|
+
options: { format: "txt", encoding, ...(cursor ? { cursor } : {}) } }, native.context);
|
|
120
|
+
const part = result.content[0], range = result.textRange;
|
|
121
|
+
if (part?.type !== "text" || !range || range.start !== offset || result.originalSha256 !== sha(original)
|
|
122
|
+
|| (representation !== undefined && result.representationSha256 !== representation)) {
|
|
123
|
+
throw new DocumentPreparationError("invalid_text");
|
|
124
|
+
}
|
|
125
|
+
representation = result.representationSha256; total = range.totalBytes; offset = range.end;
|
|
126
|
+
parts.push(part.text); cursor = result.nextCursor;
|
|
127
|
+
} while (cursor);
|
|
128
|
+
const text = parts.join("");
|
|
129
|
+
if (sha(text) !== representation || Buffer.byteLength(text) !== total) throw new DocumentPreparationError("invalid_text");
|
|
130
|
+
return { text, encoding };
|
|
131
|
+
}
|
|
132
|
+
case "utf-8": case "utf-16le": case "utf-16be": {
|
|
133
|
+
let text: string;
|
|
134
|
+
try { text = new TextDecoder(encoding, { fatal: true, ignoreBOM: true }).decode(original.subarray(skip)); }
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!(error instanceof TypeError)) throw error;
|
|
137
|
+
throw new DocumentPreparationError(options.encoding || bom || declared ? "unsupported_encoding" : "encoding_required");
|
|
138
|
+
}
|
|
139
|
+
if (text.includes("\0")) throw new DocumentPreparationError("invalid_text");
|
|
140
|
+
return { text: normalize(text), encoding };
|
|
141
|
+
}
|
|
142
|
+
default: {
|
|
143
|
+
const exhaustive: never = encoding;
|
|
144
|
+
return exhaustive;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function inertText(source: string): string {
|
|
150
|
+
const stripped = stripActive(source);
|
|
151
|
+
const separated = new HTMLRewriter().on("p,div,br,li,tr,h1,h2,h3,h4,h5,h6,pre,blockquote,section,article", {
|
|
152
|
+
element(element) { element.before("\n"); if (element.canHaveContent) element.after("\n"); },
|
|
153
|
+
}).transform(stripped);
|
|
154
|
+
const chunks: string[] = [];
|
|
155
|
+
new HTMLRewriter().onDocument({ text(chunk) { chunks.push(chunk.text); } }).transform(separated);
|
|
156
|
+
// Decode complete character-reference tokens with Bun's existing entity table, not the surrounding
|
|
157
|
+
// Markdown/HTML. Passing only a single &name; token prevents formatting, links and code interpretation.
|
|
158
|
+
return normalize(chunks.join("").replace(/&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g,
|
|
159
|
+
token => Bun.markdown.render(token, { text: text => text })));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Pure byte-to-representation work. Callers own source grants, revision CAS and publication. */
|
|
163
|
+
export async function prepareText(input: Uint8Array, rawOptions: unknown, native?: NativeTextPreparation): Promise<PreparedText> {
|
|
164
|
+
const options = parseOptions(rawOptions);
|
|
165
|
+
if (input.byteLength === 0) throw new DocumentPreparationError("empty_file");
|
|
166
|
+
if (input.byteLength > 10 * 1024 * 1024) throw new DocumentPreparationError("processing_limit", "original_bytes");
|
|
167
|
+
const original = Uint8Array.from(input);
|
|
168
|
+
const decoded = await decode(original, options, native);
|
|
169
|
+
const sourceEscaped = options.format === "html" ? Bun.escapeHTML(decoded.text) : undefined;
|
|
170
|
+
// Check expansion before building the additional text representation.
|
|
171
|
+
if (sourceEscaped !== undefined && Buffer.byteLength(sourceEscaped) > 20 * 1024 * 1024) {
|
|
172
|
+
throw new DocumentPreparationError("processing_limit", "prepared_bytes");
|
|
173
|
+
}
|
|
174
|
+
const text = options.format === "html" ? inertText(decoded.text) : decoded.text;
|
|
175
|
+
if (text.includes("\0")) throw new DocumentPreparationError("invalid_text");
|
|
176
|
+
const bytes = Buffer.byteLength(text);
|
|
177
|
+
if (bytes + Buffer.byteLength(sourceEscaped ?? "") > 20 * 1024 * 1024) throw new DocumentPreparationError("processing_limit", "prepared_bytes");
|
|
178
|
+
let scalars = 0;
|
|
179
|
+
for (const _scalar of text) scalars++;
|
|
180
|
+
const originalSha256 = sha(original);
|
|
181
|
+
const optionsSha256 = sha(JSON.stringify({ version: 1, format: options.format, encoding: options.encoding ?? null,
|
|
182
|
+
decodedEncoding: decoded.encoding, newline: "lf", html: "inert-v1" }));
|
|
183
|
+
return Object.freeze({
|
|
184
|
+
format: options.format, encoding: decoded.encoding, originalSha256, representationSha256: sha(text),
|
|
185
|
+
representationKey: sha(JSON.stringify([originalSha256, options.helperSourceSha256, options.helperBuildSha256, optionsSha256])),
|
|
186
|
+
optionsSha256, helperSourceSha256: options.helperSourceSha256, helperBuildSha256: options.helperBuildSha256,
|
|
187
|
+
text, bytes, scalars, ...(sourceEscaped === undefined ? {} : { sourceEscaped, sourceEscapedSha256: sha(sourceEscaped) }),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Internal scalar continuation, not a grant or a public opaque tool cursor. */
|
|
192
|
+
export function readPreparedText(document: PreparedText, options: { readonly cursor?: TextCursor; readonly lineLimit?: number }): PreparedTextRead {
|
|
193
|
+
const lineLimit = options.lineLimit ?? 2000, cursor = options.cursor;
|
|
194
|
+
if (!Number.isSafeInteger(lineLimit) || lineLimit < 1 || lineLimit > 2000) throw new DocumentPreparationError("invalid_document_options");
|
|
195
|
+
if (cursor && (cursor.originalSha256 !== document.originalSha256 || cursor.representationSha256 !== document.representationSha256
|
|
196
|
+
|| cursor.representationKey !== document.representationKey || !Number.isSafeInteger(cursor.offset)
|
|
197
|
+
|| cursor.offset <= 0 || cursor.offset >= document.scalars)) throw new DocumentPreparationError("invalid_cursor");
|
|
198
|
+
const start = cursor?.offset ?? 0;
|
|
199
|
+
let position = 0, end = start, bytes = 0, lines = 0;
|
|
200
|
+
const text: string[] = [];
|
|
201
|
+
for (const scalar of document.text) {
|
|
202
|
+
if (position++ < start) continue;
|
|
203
|
+
const size = Buffer.byteLength(scalar);
|
|
204
|
+
if (bytes + size > 48 * 1024 || lines >= lineLimit) break;
|
|
205
|
+
text.push(scalar); bytes += size; end++;
|
|
206
|
+
if (scalar === "\n") lines++;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
text: text.join(""), originalSha256: document.originalSha256, representationSha256: document.representationSha256,
|
|
210
|
+
range: { start, end, totalScalars: document.scalars, totalBytes: document.bytes },
|
|
211
|
+
...(end < document.scalars ? { nextCursor: { originalSha256: document.originalSha256,
|
|
212
|
+
representationSha256: document.representationSha256, representationKey: document.representationKey, offset: end } } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { HostPicker, NativeSelector, sealOriginal, type PickerScope, type SelectedSource } from "./host-selector.ts";
|
|
4
|
+
import type { SelectorPort } from "./service.ts";
|
|
5
|
+
|
|
6
|
+
/** The packaged selector, or null when the file is absent, not a plain executable, or differs from the manifest hash. */
|
|
7
|
+
export function verifiedSelectorPort(options: Parameters<typeof hostSelectorPort>[0] & { readonly expectedSha256: string }): SelectorPort | null {
|
|
8
|
+
let stat;
|
|
9
|
+
try { stat = lstatSync(options.executable); } catch { return null; }
|
|
10
|
+
if (!stat.isFile() || (stat.mode & 0o111) === 0) return null;
|
|
11
|
+
if (createHash("sha256").update(readFileSync(options.executable)).digest("hex") !== options.expectedSha256) return null;
|
|
12
|
+
return hostSelectorPort(options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Loaded only on Darwin: host-selector binds libSystem when it is imported. */
|
|
16
|
+
export function hostSelectorPort(options: {
|
|
17
|
+
readonly executable: string; readonly runtimeHome: string; readonly generation: string; readonly current: (scope: PickerScope) => boolean;
|
|
18
|
+
}): SelectorPort {
|
|
19
|
+
const native = new NativeSelector(options.executable);
|
|
20
|
+
const picker = new HostPicker({ native, runtimeHome: options.runtimeHome, generation: options.generation, current: options.current });
|
|
21
|
+
return {
|
|
22
|
+
open: scope => picker.create(scope),
|
|
23
|
+
list: (scope, id, input) => picker.list(scope, id, input),
|
|
24
|
+
resolve: (scope, id, token) => picker.resolveSelection(scope, id, token, true),
|
|
25
|
+
scopeOf: (principal, id) => picker.scopeOf(principal, id),
|
|
26
|
+
seal: (id, source: SelectedSource, signal, beforePublish) => sealOriginal(options.runtimeHome, id, source, native, signal, beforePublish),
|
|
27
|
+
};
|
|
28
|
+
}
|