zigmini 0.9.0-beta.3 → 1.0.0-beta.2
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 -4
- package/README.md +36 -4
- package/assets/agent/gateway-rules.md +29 -19
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +60 -11
- package/docs/manual.md +61 -12
- package/host/omo-bridge/agent-control.ts +274 -0
- package/host/omo-bridge/bridge.ts +1 -1
- package/host/omo-bridge/continuity-control.ts +1 -1
- package/host/omo-bridge/task-control.ts +26 -16
- package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
- package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
- package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
- package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
- package/package.json +3 -2
- package/src/agent/api.ts +215 -0
- package/src/agent/definition.ts +177 -0
- package/src/agent/fields.ts +42 -0
- package/src/agent/files.ts +192 -0
- package/src/agent/legacy-files.ts +46 -0
- package/src/agent/list.ts +21 -0
- package/src/agent-rules.ts +97 -13
- package/src/approvals.ts +6 -1
- package/src/cli/doctor.ts +7 -0
- package/src/cli/main.ts +38 -0
- package/src/cli/messages.ts +10 -2
- package/src/cli/rotate-main.ts +2 -1
- package/src/cli/rules.ts +70 -0
- package/src/cli/scan.ts +414 -0
- package/src/cli/security-import.ts +155 -0
- package/src/compact.ts +22 -0
- package/src/decisions.ts +6 -4
- package/src/engine/compat.json +3 -3
- package/src/engine/index.ts +1 -0
- package/src/engine/schema-guard.ts +12 -4
- package/src/engine/sdk.ts +8 -0
- package/src/formal-verification.ts +366 -0
- package/src/gc.ts +37 -6
- package/src/hub.ts +339 -79
- package/src/main.ts +95 -9
- package/src/maintenance.ts +1 -1
- package/src/manual.ts +9 -2
- package/src/profile-prompt.ts +73 -0
- package/src/profile.ts +33 -10
- package/src/protocol.ts +11 -2
- package/src/question-notifications.ts +4 -1
- package/src/reply-notifications.ts +7 -1
- package/src/repo-security-setting.ts +33 -0
- package/src/runtime/paths.ts +16 -0
- package/src/security/baseline.ts +172 -0
- package/src/security/dex-strings.ts +242 -0
- package/src/security/repo-scan.ts +369 -0
- package/src/security/revision-probe.ts +144 -0
- package/src/security/terms.ts +97 -0
- package/src/security-allow.ts +1 -0
- package/src/security-baseline.ts +113 -0
- package/src/security-check.ts +91 -16
- package/src/server-text.ts +38 -0
- package/src/store.ts +142 -16
- package/src/transcript.ts +7 -1
- package/src/ui-language.ts +33 -0
- package/src/update/check.ts +7 -3
- package/src/update/handoff-system.ts +24 -16
- package/src/update/handoff.ts +23 -12
- package/src/update/helper.ts +72 -36
- package/src/update/request.ts +46 -30
- package/src/update/state.ts +21 -11
- package/src/update/strategy.ts +12 -5
- package/src/update/target.ts +32 -15
- package/src/usage.ts +43 -20
- package/src/version.ts +39 -16
- package/src/voice/agents.ts +8 -3
- package/src/voice/install.ts +29 -18
- package/src/work-watch.ts +40 -18
- package/web/agent-icon.js +15 -9
- package/web/agents.d.ts +201 -0
- package/web/agents.js +813 -0
- package/web/app-download.js +32 -19
- package/web/app.js +1407 -278
- package/web/autocopy.js +6 -1
- package/web/boot.js +27 -8
- package/web/commands.js +20 -11
- package/web/decisions.js +70 -49
- package/web/devlog.js +25 -19
- package/web/diagram.js +8 -3
- package/web/enroll.js +43 -22
- package/web/gc.js +32 -19
- package/web/i18n/agent-icon.js +24 -0
- package/web/i18n/agents.js +48 -0
- package/web/i18n/app-download.js +26 -0
- package/web/i18n/autocopy.js +5 -0
- package/web/i18n/boot.js +24 -0
- package/web/i18n/commands.js +34 -0
- package/web/i18n/decisions.js +24 -0
- package/web/i18n/devlog.js +60 -0
- package/web/i18n/diagram.js +12 -0
- package/web/i18n/enroll.js +22 -0
- package/web/i18n/gc.js +38 -0
- package/web/i18n/lastsent.js +10 -0
- package/web/i18n/main.d.ts +4 -0
- package/web/i18n/main.js +527 -0
- package/web/i18n/manual.d.ts +4 -0
- package/web/i18n/manual.js +14 -0
- package/web/i18n/memo.js +30 -0
- package/web/i18n/notifications.js +16 -0
- package/web/i18n/profile.js +13 -0
- package/web/i18n/reference.js +26 -0
- package/web/i18n/security-inbox.js +40 -0
- package/web/i18n/slash.js +38 -0
- package/web/i18n/turn-stats.js +5 -0
- package/web/i18n/update.js +42 -0
- package/web/i18n/voice.js +28 -0
- package/web/i18n/wake.js +16 -0
- package/web/i18n/workid.js +10 -0
- package/web/i18n/workstatus.js +20 -0
- package/web/i18n.d.ts +20 -0
- package/web/i18n.js +63 -0
- package/web/index.html +156 -96
- package/web/lastsent.js +9 -4
- package/web/manual.d.ts +1 -1
- package/web/manual.js +11 -5
- package/web/memo.js +18 -13
- package/web/notifications.js +12 -7
- package/web/profile.js +42 -17
- package/web/reference.js +20 -15
- package/web/security-inbox.d.ts +18 -1
- package/web/security-inbox.js +173 -4
- package/web/slash.js +23 -13
- package/web/style.css +132 -3
- package/web/tooltip.js +5 -2
- package/web/turn-stats.js +6 -2
- package/web/update.js +31 -23
- package/web/voice.js +18 -8
- package/web/wake.d.ts +4 -0
- package/web/wake.js +7 -3
- package/web/workid.js +12 -2
- package/web/workstatus.js +36 -32
package/src/main.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join, normalize } from "node:path";
|
|
4
4
|
import type { Server, ServerWebSocket, WebSocketHandler } from "bun";
|
|
5
5
|
import { createSttProvider } from "./adapters/voice/stt.ts";
|
|
6
6
|
import { createTtsProvider, speakableText, splitForTts } from "./adapters/voice/tts.ts";
|
|
7
|
-
import { handleHeld, handleSecurityAllowedValues, handleSecurityCheckSetting, MessageGuard, SECURITY_CHECK_PATH, SECURITY_HELD_PATH, SECURITY_ALLOWED_PATH, SecurityCheckError, defaultChecker } from "./security-check.ts";
|
|
7
|
+
import { handleHeld, handleSecurityAllowedValues, handleSecurityBaseline, handleSecurityCheckSetting, MessageGuard, SECURITY_BASELINE_PATH, SECURITY_CHECK_PATH, SECURITY_HELD_PATH, SECURITY_ALLOWED_PATH, SecurityCheckError, defaultChecker } from "./security-check.ts";
|
|
8
|
+
import { defaultFormalDeps, FormalVerification, handleFormalVerification } from "./formal-verification.ts";
|
|
9
|
+
import { handleRepoSecurityCheckSetting, REPO_SECURITY_CHECK_PATH } from "./repo-security-setting.ts";
|
|
10
|
+
import { handleUiLanguage, readUiLanguage, UI_LANGUAGE_EVENT, UI_LANGUAGE_PATH } from "./ui-language.ts";
|
|
11
|
+
import { say, setServerLanguage } from "./server-text.ts";
|
|
12
|
+
import { SecurityBaseline } from "./security-baseline.ts";
|
|
8
13
|
import { handleVoiceInstall, handleVoiceSetting, routeVoiceApi, VOICE_INSTALL_PATH, VOICE_SETTING_PATH, voiceGate, voiceUpdated } from "./voice-api.ts";
|
|
9
14
|
import { readVoiceSetting, VoiceSetting } from "./voice-setting.ts";
|
|
10
15
|
import { applyVoiceAgents } from "./voice/agents.ts";
|
|
@@ -65,13 +70,18 @@ import { engine } from "./engine/index.ts";
|
|
|
65
70
|
import { manifestOf } from "./engine/install.ts";
|
|
66
71
|
import { launchUpdater, systemContext, systemDeps } from "./cli/launchd.ts";
|
|
67
72
|
import { acceptUpdateConsent, requestUpdate, type UpdateApiDeps } from "./update/request.ts";
|
|
68
|
-
import {
|
|
73
|
+
import { phaseLabel, readState, UPDATE_CONSENT_PATH, UPDATE_PATH, UPDATE_STATE_PATH, updateDir, UpdateWatcher } from "./update/state.ts";
|
|
69
74
|
import { resolveTarget, tarEntry } from "./update/target.ts";
|
|
70
75
|
import { registryUrl, UPDATE_CHECK_PATH, UPDATE_STATUS_PATH, UpdateChecker, versionAvailableEnvelope } from "./update/check.ts";
|
|
71
76
|
import { MANUAL_PATH, serveManual } from "./manual.ts";
|
|
72
77
|
import { clean as gcClean, GC_CLEAN_PATH, GC_PATH, type GcRoots, parseSelection as parseGcSelection, report as gcReport, systemGcDeps } from "./gc.ts";
|
|
78
|
+
import { mergeAgentDefinition, parseAgentDefinition, AgentDefinitionError } from "./agent/definition.ts";
|
|
79
|
+
import { AgentApi } from "./agent/api.ts";
|
|
80
|
+
import { AgentFileStore } from "./agent/files.ts";
|
|
81
|
+
import { agentList } from "./agent/list.ts";
|
|
73
82
|
|
|
74
83
|
// Exit 0 so launchd (KeepAlive SuccessfulExit=false) does not restart a gateway that has no consent.
|
|
84
|
+
setServerLanguage(() => readUiLanguage(runtimeHome()));
|
|
75
85
|
const consentRefusal = startRefusal(runtimeHome(), installKind(), () => loadNotice());
|
|
76
86
|
if (consentRefusal !== null) {
|
|
77
87
|
console.error(`${new Date().toISOString()} ${consentRefusal}`);
|
|
@@ -83,11 +93,16 @@ const runtime = runtimePaths();
|
|
|
83
93
|
const schemaRun = runtimePaths().run;
|
|
84
94
|
const gcHome = runtimePaths();
|
|
85
95
|
const gcRoots: GcRoots = { kind: installKind(), home: gcHome.home, backups: join(config.dataDir, "backups"), omo: gcHome.omo,
|
|
86
|
-
update: join(gcHome.home, "update"), tmp: process.env.TMPDIR || tmpdir() };
|
|
96
|
+
update: join(gcHome.home, "update"), tmp: process.env.TMPDIR || tmpdir(), agents: gcHome.agents };
|
|
87
97
|
const gcDeps = systemGcDeps(join(import.meta.dir, ".."), { ...process.env, ZIGMINI_HOME: gcHome.home });
|
|
88
98
|
const store = new Store(config.dataDir);
|
|
89
99
|
const GATEWAY_ID = gatewayId(store);
|
|
90
100
|
const GATEWAY_VERSION = (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }).version;
|
|
101
|
+
const securityBaseline = new SecurityBaseline(store, { secrets: runtime.secrets, gatewayVersion: GATEWAY_VERSION });
|
|
102
|
+
// Improvement 250: formal verification switches; installs run in the gateway process and report through this event.
|
|
103
|
+
const formalVerification = new FormalVerification(runtime.formalVerificationFile, runtime.formalToolsDir, defaultFormalDeps(), (state) => publish({
|
|
104
|
+
v: PROTOCOL_VERSION, id: crypto.randomUUID(), ts: new Date().toISOString(), conv: "*", type: "formal-verification.updated", data: state,
|
|
105
|
+
}));
|
|
91
106
|
const exchangeLimiter = new RateLimiter(EXCHANGE_LIMIT, EXCHANGE_WINDOW_MS);
|
|
92
107
|
const apkSource = new ApkSource(APK_DIR);
|
|
93
108
|
const downloadLinks = new DownloadLinks(Date.now);
|
|
@@ -154,12 +169,16 @@ let securityDecisionRefresh = () => {};
|
|
|
154
169
|
const securityEvent = (type: string, data: unknown) => publish({
|
|
155
170
|
v: PROTOCOL_VERSION, id: crypto.randomUUID(), ts: new Date().toISOString(), conv: "*", type, data,
|
|
156
171
|
});
|
|
172
|
+
const securityAllowedUpdated = (revision: number) => {
|
|
173
|
+
securityBaseline.export();
|
|
174
|
+
securityEvent("security-check.allowed-values.updated", { revision });
|
|
175
|
+
};
|
|
157
176
|
const messageGuard = new MessageGuard(defaultChecker(), runtimePaths().securityCheckFile, (held) => publish({
|
|
158
177
|
v: PROTOCOL_VERSION, id: crypto.randomUUID(), ts: new Date().toISOString(), conv: held.conv, type: "security-check.held", data: held,
|
|
159
178
|
}), { store, state: (state) => {
|
|
160
179
|
securityEvent("security-check.state", state);
|
|
161
180
|
if (state.status === "sent" || state.status === "cancelled") securityEvent("security-check.resolved", { id: state.id, outcome: state.status === "sent" ? "sent" : "dropped" });
|
|
162
|
-
}, allowedUpdated:
|
|
181
|
+
}, allowedUpdated: securityAllowedUpdated,
|
|
163
182
|
decisionUpdated: () => securityDecisionRefresh() });
|
|
164
183
|
const hub = new Hub(
|
|
165
184
|
store,
|
|
@@ -185,6 +204,8 @@ const hub = new Hub(
|
|
|
185
204
|
},
|
|
186
205
|
(e) => publish(e),
|
|
187
206
|
);
|
|
207
|
+
const agentFiles = new AgentFileStore();
|
|
208
|
+
const agentApi = new AgentApi(agentFiles, store, hub);
|
|
188
209
|
const watcher = new WorkWatcher(store, {
|
|
189
210
|
requestStatus: (workId, requestKey, instructions) => {
|
|
190
211
|
work.requestStatus(workId, requestKey, instructions);
|
|
@@ -513,7 +534,7 @@ async function handleClient(ws: ServerWebSocket<Conn>, raw: string): Promise<voi
|
|
|
513
534
|
return;
|
|
514
535
|
case "conversation.rotate":
|
|
515
536
|
await hub.rotateSession(msg.conv, "manual").catch((e) => {
|
|
516
|
-
const message = e instanceof Error && e.message === "turn_running" ? ROTATION_HELD_MESSAGE : String(e);
|
|
537
|
+
const message = e instanceof Error && e.message === "turn_running" ? say(ROTATION_HELD_MESSAGE) : String(e);
|
|
517
538
|
ws.send(envelope("error", { message }, msg.conv));
|
|
518
539
|
});
|
|
519
540
|
return;
|
|
@@ -580,9 +601,51 @@ function deviceAdmin(req: Request, url: URL): Response {
|
|
|
580
601
|
|
|
581
602
|
async function handle(req: Request, srv: Server<Conn>, trusted: boolean): Promise<Response | undefined> {
|
|
582
603
|
const url = new URL(req.url);
|
|
604
|
+
if (url.pathname === "/api/agents" || url.pathname.startsWith("/api/agents/")) {
|
|
605
|
+
return agentApi.handle(req, trusted);
|
|
606
|
+
}
|
|
583
607
|
if (url.pathname === "/internal/maintenance") return maintenance.handle(req, trusted);
|
|
584
608
|
if (url.pathname === "/internal/runtime") return runtimeApi(req, trusted);
|
|
585
609
|
if (url.pathname === "/internal/work") return hub.admitWorkRequest(() => work.handleHttp(req, trusted, omoToken));
|
|
610
|
+
if (url.pathname === "/internal/agent-list") {
|
|
611
|
+
const denied = maintenanceAuth(req, trusted, omoToken);
|
|
612
|
+
if (denied) return denied;
|
|
613
|
+
if (req.method !== "GET") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
614
|
+
return Response.json(agentList(agentFiles.list(), store.listConversations(false)));
|
|
615
|
+
}
|
|
616
|
+
if (url.pathname === "/internal/agent-history") {
|
|
617
|
+
const denied = maintenanceAuth(req, trusted, omoToken);
|
|
618
|
+
if (denied) return denied;
|
|
619
|
+
if (req.method !== "GET") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
620
|
+
const agent = url.searchParams.get("agent") ?? "";
|
|
621
|
+
const query = url.searchParams.get("query") ?? "";
|
|
622
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agent)) return Response.json({ error: "invalid_agent_id" }, { status: 400 });
|
|
623
|
+
if (query === "" || query.length > 200) return Response.json({ error: "invalid_query" }, { status: 400 });
|
|
624
|
+
const requested = Number(url.searchParams.get("limit") ?? "20");
|
|
625
|
+
const limit = Number.isInteger(requested) ? Math.min(Math.max(requested, 1), 50) : 20;
|
|
626
|
+
return Response.json({ hits: store.searchAgentHistory(agent, query, limit) });
|
|
627
|
+
}
|
|
628
|
+
if (url.pathname.startsWith("/internal/agent/")) {
|
|
629
|
+
const denied = maintenanceAuth(req, trusted, omoToken);
|
|
630
|
+
if (denied) return denied;
|
|
631
|
+
if (req.method !== "GET") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
632
|
+
const id = url.pathname.slice("/internal/agent/".length);
|
|
633
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) return Response.json({ error: "invalid_agent_id" }, { status: 400 });
|
|
634
|
+
try {
|
|
635
|
+
const root = realpathSync(join(runtimeHome(), "agents"));
|
|
636
|
+
const file = realpathSync(join(root, id, "agent.md"));
|
|
637
|
+
if (!file.startsWith(`${root}/${id}/`)) return Response.json({ error: "agent_path_outside_root" }, { status: 400 });
|
|
638
|
+
const agent = parseAgentDefinition(readFileSync(file, "utf8"));
|
|
639
|
+
const defaultsPath = join(root, "defaults.md");
|
|
640
|
+
if (existsSync(defaultsPath) && realpathSync(defaultsPath) !== defaultsPath) return Response.json({ error: "agent_defaults_symlink" }, { status: 400 });
|
|
641
|
+
const defaults = existsSync(defaultsPath) ? parseAgentDefinition(readFileSync(defaultsPath, "utf8"), "defaults") : undefined;
|
|
642
|
+
return Response.json(mergeAgentDefinition(agent, defaults));
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (error instanceof AgentDefinitionError) return Response.json({ error: "invalid_agent_definition", field: error.field }, { status: 400 });
|
|
645
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return Response.json({ error: "agent_not_found" }, { status: 404 });
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
586
649
|
if (url.pathname === "/internal/omo-check") return hub.admitWorkRequest(() => omoCheck.handle(req, trusted));
|
|
587
650
|
let deviceId: string | null = null;
|
|
588
651
|
if (trusted) {
|
|
@@ -666,8 +729,30 @@ async function handle(req: Request, srv: Server<Conn>, trusted: boolean): Promis
|
|
|
666
729
|
const response = await handleHeld(req, url, messageGuard, () => {}, { client: trusted ? "web" : "android", deviceId });
|
|
667
730
|
if (response) return response;
|
|
668
731
|
}
|
|
732
|
+
if (url.pathname === SECURITY_BASELINE_PATH || url.pathname.startsWith(`${SECURITY_BASELINE_PATH}/`) ||
|
|
733
|
+
(req.method === "PATCH" && url.pathname.startsWith(`${SECURITY_ALLOWED_PATH}/`))) {
|
|
734
|
+
const response = await handleSecurityBaseline(req, url, trusted, store, securityBaseline,
|
|
735
|
+
(revision) => securityEvent("security-check.allowed-values.updated", { revision }));
|
|
736
|
+
if (response) return response;
|
|
737
|
+
}
|
|
669
738
|
if (url.pathname === SECURITY_ALLOWED_PATH || url.pathname.startsWith(`${SECURITY_ALLOWED_PATH}/`)) {
|
|
670
|
-
const response = await handleSecurityAllowedValues(req, url, store,
|
|
739
|
+
const response = await handleSecurityAllowedValues(req, url, store, securityAllowedUpdated);
|
|
740
|
+
if (response) return response;
|
|
741
|
+
}
|
|
742
|
+
if (url.pathname === REPO_SECURITY_CHECK_PATH) {
|
|
743
|
+
const response = await handleRepoSecurityCheckSetting(req, trusted, runtime.repoSecurityCheckFile, (enabled) => publish({
|
|
744
|
+
v: PROTOCOL_VERSION, id: crypto.randomUUID(), ts: new Date().toISOString(), conv: "*", type: "repo-security-check.updated", data: { enabled },
|
|
745
|
+
}));
|
|
746
|
+
if (response) return response;
|
|
747
|
+
}
|
|
748
|
+
{
|
|
749
|
+
const response = await handleFormalVerification(req, url, trusted, formalVerification);
|
|
750
|
+
if (response) return response;
|
|
751
|
+
}
|
|
752
|
+
if (url.pathname === UI_LANGUAGE_PATH) {
|
|
753
|
+
const response = await handleUiLanguage(req, runtimeHome(), (language) => publish({
|
|
754
|
+
v: PROTOCOL_VERSION, id: crypto.randomUUID(), ts: new Date().toISOString(), conv: "*", type: UI_LANGUAGE_EVENT, data: { language },
|
|
755
|
+
}));
|
|
671
756
|
if (response) return response;
|
|
672
757
|
}
|
|
673
758
|
if (url.pathname === SECURITY_CHECK_PATH) {
|
|
@@ -688,13 +773,13 @@ async function handle(req: Request, srv: Server<Conn>, trusted: boolean): Promis
|
|
|
688
773
|
return Response.json(answer.body, { status: answer.status });
|
|
689
774
|
}
|
|
690
775
|
if (url.pathname === UPDATE_CONSENT_PATH && req.method === "POST") {
|
|
691
|
-
if (!trusted) return Response.json({ ok: false, code: "forbidden", message: "권한 동의는 맥에서만 받습니다" }, { status: 403 });
|
|
776
|
+
if (!trusted) return Response.json({ ok: false, code: "forbidden", message: say({ en: "Permission consent is only accepted on the Mac", ko: "권한 동의는 맥에서만 받습니다" }) }, { status: 403 });
|
|
692
777
|
const answer = await acceptUpdateConsent(await req.json().catch(() => null), updateApi);
|
|
693
778
|
return Response.json(answer.body, { status: answer.status });
|
|
694
779
|
}
|
|
695
780
|
if (url.pathname === UPDATE_STATE_PATH && req.method === "GET") {
|
|
696
781
|
const state = readState(runtime.home);
|
|
697
|
-
return Response.json(state === null ? null : { ...state, label:
|
|
782
|
+
return Response.json(state === null ? null : { ...state, label: phaseLabel(state.phase) });
|
|
698
783
|
}
|
|
699
784
|
// Reserve before parsing bodies: decisions, voice and auxiliary workers can
|
|
700
785
|
// otherwise cross native admission after a maintenance check.
|
|
@@ -746,6 +831,7 @@ async function routeApi(req: Request, url: URL): Promise<Response> {
|
|
|
746
831
|
return Response.json({
|
|
747
832
|
ok: true,
|
|
748
833
|
omo: omoHealth,
|
|
834
|
+
security: securityBaseline.status(),
|
|
749
835
|
...(omoHealth === SCHEMA_TOO_NEW ? { schemaGuard: schema?.message } : {}),
|
|
750
836
|
stt: stt.name,
|
|
751
837
|
tts: tts.name,
|
package/src/maintenance.ts
CHANGED
|
@@ -186,7 +186,7 @@ export async function nativeMaintenanceBlocker(
|
|
|
186
186
|
row.kind !== "interactive" ||
|
|
187
187
|
owners.get(row.sessionId) !== row.sessionPath ||
|
|
188
188
|
!record(row.context) ||
|
|
189
|
-
row.context.zgw_role !== "main"
|
|
189
|
+
(row.context.zgw_role !== "main" && row.context.zgw_role !== "agent")
|
|
190
190
|
) {
|
|
191
191
|
return "native_owner_unverified";
|
|
192
192
|
}
|
package/src/manual.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { say, type Text } from "./server-text.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The user manual (docs/manual.md, docs/manual.ko.md) for the web 설정 "매뉴얼" button and the app's work status screen.
|
|
@@ -8,6 +9,12 @@ export const MANUAL_PATH = "/api/manual";
|
|
|
8
9
|
export const MANUAL_DIR = join(import.meta.dir, "..", "docs");
|
|
9
10
|
export type ManualLang = "ko" | "en";
|
|
10
11
|
|
|
12
|
+
const UNKNOWN_LANG: Text = { en: "lang must be ko or en.", ko: "lang은 ko 또는 en입니다." };
|
|
13
|
+
const MANUAL_MISSING: Text = {
|
|
14
|
+
en: "This installation has no manual file. Update the gateway.",
|
|
15
|
+
ko: "이 설치에는 매뉴얼 파일이 없습니다. 게이트웨이를 업데이트하세요.",
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
/** The `lang` query value as a language: missing or empty is Korean; anything but `ko` or `en` is null. */
|
|
12
19
|
export function manualLang(value: string | null): ManualLang | null {
|
|
13
20
|
if (value === null || value === "" || value === "ko") return "ko";
|
|
@@ -22,10 +29,10 @@ export function manualFile(lang: ManualLang): string {
|
|
|
22
29
|
export async function serveManual(req: Request, url: URL, dir: string = MANUAL_DIR): Promise<Response> {
|
|
23
30
|
if (req.method !== "GET") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
24
31
|
const lang = manualLang(url.searchParams.get("lang"));
|
|
25
|
-
if (lang === null) return Response.json({ error: "unknown_lang", message:
|
|
32
|
+
if (lang === null) return Response.json({ error: "unknown_lang", message: say(UNKNOWN_LANG) }, { status: 400 });
|
|
26
33
|
const file = Bun.file(join(dir, manualFile(lang)));
|
|
27
34
|
if (!(await file.exists())) {
|
|
28
|
-
return Response.json({ error: "manual_missing", message:
|
|
35
|
+
return Response.json({ error: "manual_missing", message: say(MANUAL_MISSING) }, { status: 404 });
|
|
29
36
|
}
|
|
30
37
|
return new Response(file, { headers: { "content-type": "text/markdown; charset=utf-8", "cache-control": "no-cache", "content-language": lang } });
|
|
31
38
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NICKNAME_DEFAULT, type Profile } from "./profile.ts";
|
|
2
|
+
import { isTextOf } from "./server-text.ts";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { runtimePaths } from "./runtime/paths.ts";
|
|
5
|
+
|
|
6
|
+
export type PromptProfile = {
|
|
7
|
+
readonly nickname: string | undefined;
|
|
8
|
+
readonly agentName: string;
|
|
9
|
+
};
|
|
10
|
+
export const PROFILE_TIMEOUT_MS = 1_000;
|
|
11
|
+
|
|
12
|
+
/** Read once per turn; a failed request keeps this session's last known profile. */
|
|
13
|
+
export async function readPromptProfile(fallback: PromptProfile): Promise<PromptProfile> {
|
|
14
|
+
try {
|
|
15
|
+
const base = process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
16
|
+
const response = await fetch(new URL("/api/profile", base), { signal: AbortSignal.timeout(PROFILE_TIMEOUT_MS) });
|
|
17
|
+
if (!response.ok) throw new Error("profile_http");
|
|
18
|
+
const data: unknown = await response.json();
|
|
19
|
+
const profile = parseProfile(data);
|
|
20
|
+
if (!profile) throw new Error("profile_invalid");
|
|
21
|
+
return profile;
|
|
22
|
+
} catch { // This HTTP boundary deliberately handles unavailable or invalid gateway responses.
|
|
23
|
+
console.warn("gateway profile: keeping last known values");
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseProfile(data: unknown): Profile | undefined {
|
|
29
|
+
if (typeof data !== "object" || data === null || !("nickname" in data) || !("agentName" in data)) return undefined;
|
|
30
|
+
if (typeof data.nickname !== "string" || typeof data.agentName !== "string") return undefined;
|
|
31
|
+
const agentName = data.agentName.replace(/\s+/g, " ").trim();
|
|
32
|
+
if (!agentName) return undefined;
|
|
33
|
+
return { nickname: data.nickname, agentName };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Profile data is quoted and kept on one line, independently of rule-file toggles. */
|
|
37
|
+
export function nicknamePrompt(nickname: string | undefined): string {
|
|
38
|
+
if (nickname === undefined) return "The user's gateway profile nickname is unavailable.";
|
|
39
|
+
const name = promptName(nickname);
|
|
40
|
+
if (!name || isTextOf(name, NICKNAME_DEFAULT)) return "The user has not set a gateway profile nickname. Do not use the built-in default as their name.";
|
|
41
|
+
return `The user's gateway profile nickname is ${JSON.stringify(name)}. Address the user by it as the rules say.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function promptName(name: string): string {
|
|
45
|
+
return name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Only the name refreshes; the agent's definition, tools and memory keep their session snapshots. */
|
|
49
|
+
export async function readAgentPromptName(id: string, fallback: string): Promise<string> {
|
|
50
|
+
try {
|
|
51
|
+
const token = readFileSync(runtimePaths().omoTokenFile, "utf8").trim();
|
|
52
|
+
const base = process.env.ZGW_WORK_URL ?? `http://127.0.0.1:${process.env.ZGW_PORT ?? "8700"}`;
|
|
53
|
+
const response = await fetch(new URL(`/internal/agent/${encodeURIComponent(id)}`, base), {
|
|
54
|
+
headers: { authorization: `Bearer ${token}` }, redirect: "error", signal: AbortSignal.timeout(5_000),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) throw new Error("agent_profile_http");
|
|
57
|
+
const data: unknown = await response.json();
|
|
58
|
+
if (typeof data !== "object" || data === null || !("fields" in data) ||
|
|
59
|
+
typeof data.fields !== "object" || data.fields === null || !("name" in data.fields) ||
|
|
60
|
+
typeof data.fields.name !== "string") throw new Error("agent_profile_invalid");
|
|
61
|
+
const name = promptName(data.fields.name);
|
|
62
|
+
if (!name) throw new Error("agent_profile_name_missing");
|
|
63
|
+
return name;
|
|
64
|
+
} catch { // This HTTP boundary retains the validated session-start name on refresh failure.
|
|
65
|
+
console.warn("agent profile: keeping last known name");
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function agentNamePrompt(name: string): string {
|
|
71
|
+
const quoted = JSON.stringify(promptName(name));
|
|
72
|
+
return `Your name is ${quoted}. The user calls you ${quoted}.`;
|
|
73
|
+
}
|
package/src/profile.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// The owner's profile nickname and the agent's (gateway main) name, kept in the gateway settings so every web browser,
|
|
2
2
|
// the phone and the main session see the same names. The web (web/profile.js) applies the same rules for its local cache.
|
|
3
|
+
import { isTextOf, say, type Text } from "./server-text.ts";
|
|
3
4
|
import type { Store } from "./store.ts";
|
|
4
5
|
|
|
5
6
|
export const NICKNAME_SETTING = "profile.nickname";
|
|
6
|
-
|
|
7
|
+
/** Built-in default names. They follow the saved UI language; a stored built-in default of either language reads as the current one. */
|
|
8
|
+
export const NICKNAME_DEFAULT: Text = { en: "Me", ko: "나" };
|
|
7
9
|
export const NICKNAME_MAX = 20;
|
|
8
10
|
|
|
9
11
|
export const AGENT_NAME_SETTING = "profile.agentName";
|
|
10
|
-
export const
|
|
12
|
+
export const AGENT_NAME_DEFAULT: Text = { en: "Zig", ko: "지그" };
|
|
11
13
|
/** Display-width units: Hangul, CJK and full-width characters count 2, ASCII and half-width characters 1. */
|
|
12
14
|
export const AGENT_NAME_WIDTH = 20;
|
|
13
15
|
|
|
@@ -19,10 +21,31 @@ export interface Profile {
|
|
|
19
21
|
export type ProfileError = "nickname_required" | "agent_name_required" | "agent_name_too_long";
|
|
20
22
|
export type ProfileSave = { readonly profile: Profile } | { readonly error: ProfileError };
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
export function
|
|
24
|
+
/** The nickname default in the current UI language. */
|
|
25
|
+
export function defaultNickname(): string {
|
|
26
|
+
return say(NICKNAME_DEFAULT);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The agent name default in the current UI language. */
|
|
30
|
+
export function defaultAgentName(): string {
|
|
31
|
+
return say(AGENT_NAME_DEFAULT);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The user's own nickname: whitespace collapsed and trimmed, at most NICKNAME_MAX characters; "" for blank or a built-in default. */
|
|
35
|
+
function ownNickname(value: string): string {
|
|
36
|
+
const name = [...value.replace(/\s+/g, " ").trim()].slice(0, NICKNAME_MAX).join("");
|
|
37
|
+
return isTextOf(name, NICKNAME_DEFAULT) ? "" : name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The user's own agent name: whitespace collapsed and trimmed; "" for blank or a built-in default. */
|
|
41
|
+
function ownAgentName(value: string): string {
|
|
24
42
|
const name = value.replace(/\s+/g, " ").trim();
|
|
25
|
-
return name ?
|
|
43
|
+
return isTextOf(name, AGENT_NAME_DEFAULT) ? "" : name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The nickname to show: the user's own, else the current language's default. */
|
|
47
|
+
export function normalizeNickname(value: string): string {
|
|
48
|
+
return ownNickname(value) || defaultNickname();
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
/** True for a code point shown two columns wide: Hangul, CJK, full-width forms and wide emoji. */
|
|
@@ -41,12 +64,12 @@ export function displayWidth(text: string): number {
|
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
export function normalizeAgentName(value: string): string {
|
|
44
|
-
return value
|
|
67
|
+
return ownAgentName(value) || defaultAgentName();
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
function storedAgentName(value: string | null): string {
|
|
48
|
-
const name =
|
|
49
|
-
return displayWidth(name) <= AGENT_NAME_WIDTH ? name :
|
|
71
|
+
const name = ownAgentName(value ?? "");
|
|
72
|
+
return name && displayWidth(name) <= AGENT_NAME_WIDTH ? name : defaultAgentName();
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
export function readProfile(store: Pick<Store, "getSetting">): Profile {
|
|
@@ -66,9 +89,9 @@ export function saveProfile(store: Pick<Store, "getSetting" | "setSetting">, bod
|
|
|
66
89
|
if (nickname === undefined && agentName === undefined) return { error: "nickname_required" };
|
|
67
90
|
if (nickname !== undefined && typeof nickname !== "string") return { error: "nickname_required" };
|
|
68
91
|
if (agentName !== undefined && typeof agentName !== "string") return { error: "agent_name_required" };
|
|
69
|
-
const name = agentName === undefined ? undefined :
|
|
92
|
+
const name = agentName === undefined ? undefined : ownAgentName(agentName);
|
|
70
93
|
if (name !== undefined && displayWidth(name) > AGENT_NAME_WIDTH) return { error: "agent_name_too_long" };
|
|
71
|
-
if (nickname !== undefined) store.setSetting(NICKNAME_SETTING,
|
|
94
|
+
if (nickname !== undefined) store.setSetting(NICKNAME_SETTING, ownNickname(nickname));
|
|
72
95
|
if (name !== undefined) store.setSetting(AGENT_NAME_SETTING, name);
|
|
73
96
|
return { profile: readProfile(store) };
|
|
74
97
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import type { Text } from "./server-text.ts";
|
|
2
|
+
|
|
1
3
|
export const PROTOCOL_VERSION = 1;
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The `error` message a refused `conversation.rotate` gets when a turn or an open wait holds the session (hub `turn_running`).
|
|
7
|
+
* The sender picks the language with say(); a reader compares with isTextOf() so both languages are recognized.
|
|
8
|
+
*/
|
|
9
|
+
export const ROTATION_HELD_MESSAGE: Text = {
|
|
10
|
+
en: "The same session is kept because some work is in progress or not yet confirmed.",
|
|
11
|
+
ko: "진행 중이거나 확인되지 않은 작업이 있어 같은 세션을 유지합니다.",
|
|
12
|
+
};
|
|
5
13
|
|
|
6
14
|
export type Channel = "web" | "app" | "home" | "hud" | "discord";
|
|
7
15
|
export type Trust = "owner" | "owner-voice" | "unknown";
|
|
@@ -51,6 +59,7 @@ export interface ConversationInfo {
|
|
|
51
59
|
title: string;
|
|
52
60
|
threadId: string | null;
|
|
53
61
|
createdAt: string;
|
|
62
|
+
readonly agentId: string | null;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
export interface ConversationActivity {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Envelope } from "./protocol.ts";
|
|
2
|
+
import { say, type Text } from "./server-text.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TITLE: Text = { en: "An answer is needed", ko: "답변이 필요합니다" };
|
|
2
5
|
|
|
3
6
|
export interface QuestionNotification {
|
|
4
7
|
readonly id: string;
|
|
@@ -35,7 +38,7 @@ function questionText(data: Record<string, unknown>, fallback: string): string {
|
|
|
35
38
|
function notificationOf(data: unknown, envelopeConv = "*"): QuestionNotification | null {
|
|
36
39
|
if (!isRecord(data) || data.method !== "question" || typeof data.actionId !== "string" || !data.actionId) return null;
|
|
37
40
|
const conv = typeof data.conv === "string" && data.conv ? data.conv : envelopeConv;
|
|
38
|
-
const title = typeof data.title === "string" && data.title.trim() ? data.title.trim() :
|
|
41
|
+
const title = typeof data.title === "string" && data.title.trim() ? data.title.trim() : say(DEFAULT_TITLE);
|
|
39
42
|
return {
|
|
40
43
|
id: `question:${data.actionId}`,
|
|
41
44
|
conv,
|
|
@@ -64,10 +64,16 @@ export class ReplyNotifications<Connection extends object> {
|
|
|
64
64
|
/** The read's revision is captured before RPC, so a toggle cannot admit an older in-flight read. */
|
|
65
65
|
observe(source: NativeReplySource, entries: readonly NativeReplyEntry[], revision: string): void {
|
|
66
66
|
if (revision !== this.revision()) return;
|
|
67
|
+
const conv = this.store.getConversation(source.conv);
|
|
68
|
+
const applied = conv?.agentId
|
|
69
|
+
? this.store.getEvent(this.store.lastSeq(source.conv, "agent.applied")) : null;
|
|
70
|
+
const agentNotify = !conv?.agentId || (applied !== null &&
|
|
71
|
+
applied.seq > this.store.lastSeq(source.conv, "session.rotated") &&
|
|
72
|
+
typeof applied.data === "object" && applied.data !== null && "notify" in applied.data && applied.data.notify === true);
|
|
67
73
|
for (const entry of entries) {
|
|
68
74
|
if (typeof entry.id !== "string" || !entry.id) throw new Error("reply_history_entry_without_id");
|
|
69
75
|
const text = replyText(entry);
|
|
70
|
-
const notification = this.settings().mode === "all-replies" && text !== null ? {
|
|
76
|
+
const notification = this.settings().mode === "all-replies" && agentNotify && text !== null ? {
|
|
71
77
|
id: crypto.randomUUID(), conv: source.conv, title: source.title, text,
|
|
72
78
|
...(source.workId ? { workId: source.workId } : {}),
|
|
73
79
|
} : null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readVoiceSetting, writeVoiceSetting } from "./voice-setting.ts";
|
|
2
|
+
import type { Language } from "./cli/messages.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Improvement 250: the output check switch ("결과물 검사", the repository security check), separate from the check of
|
|
6
|
+
* messages to the engine (security-check.ts). On (the default) `zigmini scan` runs and the package agent rules tell
|
|
7
|
+
* sessions to run it before a merge. Off makes `zigmini scan` stop with SCAN_DISABLED_EXIT and removes that rule from
|
|
8
|
+
* new sessions. This repository's test:all security checks use the scan library directly and do not read this switch:
|
|
9
|
+
* the "check before a dev merge" duty is the operator's rule, not a gateway feature.
|
|
10
|
+
*/
|
|
11
|
+
export const SCAN_DISABLED_EXIT = 4;
|
|
12
|
+
const DISABLED_TEXT = {
|
|
13
|
+
en: "Output check is off in the gateway settings; zigmini scan did not run (exit 4). Turn on \"Output check\" to scan.",
|
|
14
|
+
ko: "게이트웨이 설정에서 결과물 검사가 꺼져 있어 zigmini scan을 실행하지 않았습니다(종료 코드 4). 검사하려면 \"결과물 검사\"를 켜세요.",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/** The notice when the switch is off, else null; --help still works while it is off. */
|
|
18
|
+
export function scanDisabledNotice(file: string, argv: readonly string[], lang: Language): string | null {
|
|
19
|
+
if (argv.includes("--help") || argv.includes("-h") || readVoiceSetting(file)) return null;
|
|
20
|
+
return DISABLED_TEXT[lang];
|
|
21
|
+
}
|
|
22
|
+
export const REPO_SECURITY_CHECK_PATH = "/api/settings/repo-security-check";
|
|
23
|
+
|
|
24
|
+
export async function handleRepoSecurityCheckSetting(req: Request, trusted: boolean, file: string, publish: (enabled: boolean) => void): Promise<Response | null> {
|
|
25
|
+
if (req.method === "GET") return Response.json({ enabled: readVoiceSetting(file) });
|
|
26
|
+
if (req.method !== "PUT") return null;
|
|
27
|
+
if (!trusted) return Response.json({ error: "forbidden" }, { status: 403 });
|
|
28
|
+
const body = (await req.json().catch(() => null)) as { enabled?: unknown } | null;
|
|
29
|
+
if (typeof body?.enabled !== "boolean") return Response.json({ error: "enabled_required" }, { status: 400 });
|
|
30
|
+
writeVoiceSetting(file, body.enabled);
|
|
31
|
+
publish(body.enabled);
|
|
32
|
+
return Response.json({ enabled: body.enabled });
|
|
33
|
+
}
|
package/src/runtime/paths.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface RuntimePorts {
|
|
|
34
34
|
|
|
35
35
|
export interface RuntimePaths {
|
|
36
36
|
readonly home: string;
|
|
37
|
+
readonly agents: string;
|
|
37
38
|
readonly layout: Layout;
|
|
38
39
|
readonly data: string;
|
|
39
40
|
readonly transcripts: string;
|
|
@@ -42,6 +43,9 @@ export interface RuntimePaths {
|
|
|
42
43
|
readonly scheduleFile: string;
|
|
43
44
|
readonly voiceSettingFile: string;
|
|
44
45
|
readonly securityCheckFile: string;
|
|
46
|
+
readonly repoSecurityCheckFile: string;
|
|
47
|
+
readonly formalVerificationFile: string;
|
|
48
|
+
readonly formalToolsDir: string;
|
|
45
49
|
readonly logs: string;
|
|
46
50
|
readonly secrets: string;
|
|
47
51
|
readonly omoTokenFile: string;
|
|
@@ -102,6 +106,7 @@ export function runtimePaths(env: Env = process.env, exists: (path: string) => b
|
|
|
102
106
|
const suffix = slotSuffix(slot);
|
|
103
107
|
return {
|
|
104
108
|
home,
|
|
109
|
+
agents: join(home, "agents"),
|
|
105
110
|
layout,
|
|
106
111
|
data,
|
|
107
112
|
transcripts: value(env, "ZGW_TRANSCRIPT_DIR") ?? join(data, "transcripts"),
|
|
@@ -110,6 +115,9 @@ export function runtimePaths(env: Env = process.env, exists: (path: string) => b
|
|
|
110
115
|
scheduleFile: value(env, "ZGW_SCHEDULE_FILE") ?? join(data, "schedule.json"),
|
|
111
116
|
voiceSettingFile: value(env, "ZGW_VOICE_SETTING_FILE") ?? join(data, "voice-setting"),
|
|
112
117
|
securityCheckFile: value(env, "ZGW_SECURITY_CHECK_FILE") ?? join(data, "security-check"),
|
|
118
|
+
repoSecurityCheckFile: value(env, "ZGW_REPO_SECURITY_CHECK_FILE") ?? join(data, "repo-security-check"),
|
|
119
|
+
formalVerificationFile: value(env, "ZGW_FORMAL_VERIFICATION_FILE") ?? join(data, "formal-verification.json"),
|
|
120
|
+
formalToolsDir: value(env, "ZGW_FORMAL_TOOLS_DIR") ?? join(home, "formal-tools"),
|
|
113
121
|
logs: join(home, "logs"),
|
|
114
122
|
secrets,
|
|
115
123
|
omoTokenFile: value(env, "ZGW_OMO_TOKEN_FILE") ?? join(secrets, "omo-ws-token"),
|
|
@@ -135,3 +143,11 @@ export function runtimePaths(env: Env = process.env, exists: (path: string) => b
|
|
|
135
143
|
},
|
|
136
144
|
};
|
|
137
145
|
}
|
|
146
|
+
|
|
147
|
+
/** Baseline consumers share runtime home resolution without changing the gateway's existing path object. */
|
|
148
|
+
export function securityBaselinePaths(env: Env = process.env, paths: Pick<RuntimePaths, "secrets"> = runtimePaths(env)) {
|
|
149
|
+
return {
|
|
150
|
+
securityBaselineFile: value(env, "ZGW_SECURITY_BASELINE_FILE") ?? join(paths.secrets, "security-baseline.json"),
|
|
151
|
+
legacyPrivateStringsFile: value(env, "ZIGMINI_PRIVATE_STRINGS") ?? join(value(env, "HOME") ?? homedir(), ".zigmini-release", "private-strings"),
|
|
152
|
+
};
|
|
153
|
+
}
|