cartethyia 2.7.1 → 2.7.3
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/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-D-J75hBM.js → BannedPage-2TJD37Hb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-QoJ6foxo.js → CombosPage-JEbYJFxw.js} +1 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DpNRay4C.js → ConsoleLogPage-C3xx_inM.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DubdQcvX.js → LoginPage-C4w7vea-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-ATpaxrbl.js → QuotaPage-Ci5O4BnH.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CETophpx.js → SettingsPage-CDZjTt87.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CAK-EN9z.js → SortableList-BAL5npBC.js} +1 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-B7W2_RhT.js → StudioPage-TP7voAQ0.js} +3 -3
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-6T4EFDjW.js → UsagePage-Cj_fEzhg.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/{assert-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BLBLxX0h.js → entry-j40LJotO.js} +3 -3
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-mpBPqAQ8.js → formatters-BTScrQpe.js} +1 -1
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D6QzEC_n.js → index-cPrX5vn6.js} +2 -2
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/{select-Cj5Nw43g.js → select-B3iTetHN.js} +1 -1
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/{settings-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +117 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/package.json +1 -1
- package/src/config/network.ts +1 -1
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +423 -410
- package/src/console/api-keys/routes.ts +118 -19
- package/src/console/backup/export-endpoint.ts +8 -11
- package/src/console/backup/routes.ts +40 -8
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/contracts.ts +10 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/domain-registration.ts +2 -0
- package/src/console/quota/account-quota-tenant.ts +8 -1
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/share/share-router.ts +39 -6
- package/src/console/share/share-stats.ts +64 -25
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +281 -226
- package/src/network/pool/agent.ts +7 -0
- package/src/observability/event-query.ts +118 -7
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/schema.ts +102 -0
- package/src/persistence/stores/api-key-store.ts +753 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +57 -1
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/providers/default-registry.ts +1 -0
- package/src/providers/integrations/antigravity/antigravity.ts +20 -23
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-media.ts +5 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
- package/src/providers/integrations/cline/cline.ts +7 -0
- package/src/providers/integrations/github/github.ts +20 -1
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/ollamacloud-models.ts +50 -0
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +24 -4
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
- package/src/providers/integrations/qoder/qoder.ts +1375 -1641
- package/src/providers/operations/account-error-classification.ts +1682 -1660
- package/src/providers/operations/account-health-service.ts +619 -621
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +15 -2
- package/src/security/access-control.ts +19 -0
- package/src/security/api-key-auth.ts +14 -1
- package/src/security/cli-client-fingerprint.ts +9 -2
- package/src/transport/dispatch/empty-stream.ts +224 -224
- package/src/transport/dispatch/proxy-request.ts +31 -2
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/failure-policy.ts +33 -0
- package/src/transport/request/preparer.ts +17 -1
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +41 -12
- package/src/transport/routing/route-model.ts +53 -0
- package/src/transport/routing/router.ts +306 -228
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +0 -9
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +0 -1
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery for Qoder's INTL text-mode tool-call echo.
|
|
3
|
+
*
|
|
4
|
+
* Qoder's own wire is structured `tool_calls` deltas, but the DeepSeek model
|
|
5
|
+
* sometimes *also* writes a tool call as prose with the marker:
|
|
6
|
+
*
|
|
7
|
+
* assistant 请求调用工具
|
|
8
|
+
* {"name":"write","arguments":"…"},{"name":"write","arguments":"…"}
|
|
9
|
+
*
|
|
10
|
+
* Left alone the JSON reaches the caller as visible content while nothing runs.
|
|
11
|
+
* This module owns the single tolerant parser for that echo family (JSON
|
|
12
|
+
* array/object, bare comma-separated list, unescaped inner quotes). It does
|
|
13
|
+
* NOT parse DSML/XML markup — that family is gone; only the marker+JSON echo
|
|
14
|
+
* the model actually emits is recovered here.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
|
|
18
|
+
/** One recovered call, in the shape the canonical events want. */
|
|
19
|
+
export interface QoderEchoCall {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly arguments: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Any spelling of the INTL tool-call leak marker. */
|
|
26
|
+
export const LEAK_MARKER_ANY_RE = /[\[<(]*\s*assistant\s*请求调用工具\s*[\]>)]?/;
|
|
27
|
+
|
|
28
|
+
/** Canonical marker literals used to seed prefix matching. */
|
|
29
|
+
export const LEAK_MARKER_LITERALS: readonly string[] = [
|
|
30
|
+
"[assistant 请求调用工具]",
|
|
31
|
+
"<assistant 请求调用工具>",
|
|
32
|
+
"assistant 请求调用工具",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Escapes literal control characters that occur inside JSON string values.
|
|
37
|
+
* DeepSeek often writes multi-line argument content as real newlines instead
|
|
38
|
+
* of `\n` escapes; only characters inside strings are escaped.
|
|
39
|
+
*/
|
|
40
|
+
function escapeControlInsideStrings(text: string): string {
|
|
41
|
+
let out = "";
|
|
42
|
+
let inString = false;
|
|
43
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
44
|
+
const ch = text[i] ?? "";
|
|
45
|
+
if (inString) {
|
|
46
|
+
if (ch === "\\") {
|
|
47
|
+
out += ch;
|
|
48
|
+
i += 1;
|
|
49
|
+
if (i < text.length) out += text[i];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === '"') {
|
|
53
|
+
inString = false;
|
|
54
|
+
out += ch;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "\n") {
|
|
58
|
+
out += "\\n";
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "\r") {
|
|
62
|
+
out += "\\r";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "\t") {
|
|
66
|
+
out += "\\t";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch.charCodeAt(0) < 0x20) {
|
|
70
|
+
out += "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out += ch;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '"') inString = true;
|
|
77
|
+
out += ch;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Escapes bare quotes that belong inside a JSON string value. */
|
|
83
|
+
function repairJsonQuotes(text: string): string {
|
|
84
|
+
const ph = "\u0000";
|
|
85
|
+
return text.replace(/\\"/g, ph).replace(/"/g, '\\"').replaceAll(ph, '\\"');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rebuilds the leaked echo when strict parsing fails: inner quotes unescaped
|
|
90
|
+
* (`[{"name":"read","arguments":"{"path":"a"}"},…]`) or a bare comma-separated
|
|
91
|
+
* object list. Scans top-level objects without relying on JSON validity.
|
|
92
|
+
* Returns null when the text is not that shape at all.
|
|
93
|
+
*/
|
|
94
|
+
function salvageEchoArray(rest: string): unknown {
|
|
95
|
+
const src = rest.trim();
|
|
96
|
+
if (src.length === 0) return null;
|
|
97
|
+
const calls: Array<{ name: string; arguments: string }> = [];
|
|
98
|
+
let i = 0;
|
|
99
|
+
const bracketed = src[0] === "[";
|
|
100
|
+
if (bracketed) i = 1;
|
|
101
|
+
const skipWs = (): void => {
|
|
102
|
+
while (
|
|
103
|
+
i < src.length &&
|
|
104
|
+
(src[i] === " " || src[i] === "\t" || src[i] === "\n" || src[i] === "\r")
|
|
105
|
+
) {
|
|
106
|
+
i += 1;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const NAME_RE = /"name"\s*:\s*"([^"]{1,200})"/y;
|
|
110
|
+
const ARG_KEY_RE = /"arguments"\s*:\s*"/y;
|
|
111
|
+
|
|
112
|
+
for (;;) {
|
|
113
|
+
skipWs();
|
|
114
|
+
if (i >= src.length) break;
|
|
115
|
+
if (bracketed && src[i] === "]") break;
|
|
116
|
+
if (src[i] !== "{") return null;
|
|
117
|
+
i += 1;
|
|
118
|
+
skipWs();
|
|
119
|
+
NAME_RE.lastIndex = i;
|
|
120
|
+
const nameMatch = NAME_RE.exec(src);
|
|
121
|
+
if (!nameMatch) return null;
|
|
122
|
+
const name = nameMatch[1] ?? "";
|
|
123
|
+
i = NAME_RE.lastIndex;
|
|
124
|
+
skipWs();
|
|
125
|
+
if (i < src.length && src[i] === ",") {
|
|
126
|
+
i += 1;
|
|
127
|
+
skipWs();
|
|
128
|
+
}
|
|
129
|
+
ARG_KEY_RE.lastIndex = i;
|
|
130
|
+
if (!ARG_KEY_RE.exec(src)) return null;
|
|
131
|
+
const argStart = ARG_KEY_RE.lastIndex;
|
|
132
|
+
let rawEnd = -1;
|
|
133
|
+
for (let j = argStart; j + 1 < src.length; j += 1) {
|
|
134
|
+
if (src[j] === "}" && src[j + 1] === '"') {
|
|
135
|
+
rawEnd = j;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (rawEnd < 0) return null;
|
|
140
|
+
const raw = src.slice(argStart, rawEnd + 1);
|
|
141
|
+
const parsedArgs = [
|
|
142
|
+
raw,
|
|
143
|
+
escapeControlInsideStrings(raw),
|
|
144
|
+
repairJsonQuotes(raw),
|
|
145
|
+
escapeControlInsideStrings(repairJsonQuotes(raw)),
|
|
146
|
+
].find((candidate) => {
|
|
147
|
+
try {
|
|
148
|
+
JSON.parse(candidate);
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (parsedArgs === undefined) return null;
|
|
155
|
+
calls.push({ name, arguments: parsedArgs });
|
|
156
|
+
i = rawEnd + 2;
|
|
157
|
+
skipWs();
|
|
158
|
+
if (i >= src.length || src[i] !== "}") return null;
|
|
159
|
+
i += 1;
|
|
160
|
+
skipWs();
|
|
161
|
+
if (i < src.length && src[i] === ",") {
|
|
162
|
+
i += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (i < src.length && bracketed && src[i] === "]") continue;
|
|
166
|
+
if (i < src.length) return null;
|
|
167
|
+
}
|
|
168
|
+
return calls.length > 0 ? calls : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* True when the text is clearly an attempted tool-protocol echo rather than
|
|
173
|
+
* ordinary prose: a leak marker followed by the start of a JSON array/object,
|
|
174
|
+
* or of a tool-style tag (`<tool_call>`, `<execute …/>`, angle-wrapped
|
|
175
|
+
* `<{…}>`). Used by the fail-closed decision: an echo that cannot be recovered
|
|
176
|
+
* fails the turn instead of surfacing protocol text as visible content.
|
|
177
|
+
*/
|
|
178
|
+
export function isQoderEchoAttempt(text: string): boolean {
|
|
179
|
+
const fence = text.match(/^```[a-z]*\s*\n?/i);
|
|
180
|
+
const src = fence && fence[0] ? text.slice(fence[0].length) : text;
|
|
181
|
+
const marker = LEAK_MARKER_ANY_RE.exec(src);
|
|
182
|
+
if (!marker) return false;
|
|
183
|
+
const rest = src.slice(marker.index + marker[0].length).trim();
|
|
184
|
+
return (
|
|
185
|
+
rest.startsWith("[") ||
|
|
186
|
+
rest.startsWith("{") ||
|
|
187
|
+
/^<[a-zA-Z0-9_.:-]+\b/.test(rest) ||
|
|
188
|
+
(rest.startsWith("<[") || rest.startsWith("<{"))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Recovers tool calls the model echoed back as text. Only a well-formed echo
|
|
194
|
+
* whose every entry names a tool and carries parseable arguments is accepted;
|
|
195
|
+
* anything else returns no calls and the original text.
|
|
196
|
+
*/
|
|
197
|
+
export function parseQoderTextToolCalls(
|
|
198
|
+
text: string,
|
|
199
|
+
allowedNames?: ReadonlySet<string>,
|
|
200
|
+
): { calls: QoderEchoCall[]; clean: string } {
|
|
201
|
+
if (text.length === 0) {
|
|
202
|
+
return { calls: [], clean: text };
|
|
203
|
+
}
|
|
204
|
+
const fence = text.match(/^```[a-z]*\s*\n?/i);
|
|
205
|
+
const src = fence && fence[0] ? text.slice(fence[0].length) : text;
|
|
206
|
+
const marker = LEAK_MARKER_ANY_RE.exec(src);
|
|
207
|
+
if (!marker) {
|
|
208
|
+
return { calls: [], clean: text };
|
|
209
|
+
}
|
|
210
|
+
const head = src.slice(0, marker.index).trim();
|
|
211
|
+
let rest = src.slice(marker.index + marker[0].length).trim();
|
|
212
|
+
rest = rest.replace(/\s*```\s*$/, "").trim();
|
|
213
|
+
|
|
214
|
+
let parsed: unknown;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(rest);
|
|
217
|
+
} catch {
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(escapeControlInsideStrings(rest));
|
|
220
|
+
} catch {
|
|
221
|
+
// A bare comma-separated object list (`{...},{...}`) is not valid JSON
|
|
222
|
+
// on its own, but is exactly the echo the model emits. Wrap it in an
|
|
223
|
+
// array before falling back to salvage.
|
|
224
|
+
let wrapped: unknown;
|
|
225
|
+
try {
|
|
226
|
+
wrapped = JSON.parse(`[${rest}]`);
|
|
227
|
+
} catch {
|
|
228
|
+
wrapped = undefined;
|
|
229
|
+
}
|
|
230
|
+
const salvaged = salvageEchoArray(rest);
|
|
231
|
+
if (salvaged === null && wrapped === undefined) {
|
|
232
|
+
return { calls: [], clean: text };
|
|
233
|
+
}
|
|
234
|
+
parsed = salvaged ?? wrapped;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(parsed) && parsed.length === 0) return { calls: [], clean: text };
|
|
238
|
+
|
|
239
|
+
const items: unknown[] = Array.isArray(parsed) ? parsed : [parsed];
|
|
240
|
+
const calls: QoderEchoCall[] = [];
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
if (typeof item !== "object" || item === null) return { calls: [], clean: text };
|
|
243
|
+
const rec = item as Record<string, unknown>;
|
|
244
|
+
const name = typeof rec["name"] === "string" ? rec["name"].trim() : "";
|
|
245
|
+
if (name.length === 0) return { calls: [], clean: text };
|
|
246
|
+
if (allowedNames && !allowedNames.has(name)) return { calls: [], clean: text };
|
|
247
|
+
const rawArgs = rec["arguments"];
|
|
248
|
+
let args: string;
|
|
249
|
+
if (typeof rawArgs === "object" && rawArgs !== null) args = JSON.stringify(rawArgs);
|
|
250
|
+
else if (typeof rawArgs === "string") args = rawArgs;
|
|
251
|
+
else return { calls: [], clean: text };
|
|
252
|
+
const argsText = args.trim();
|
|
253
|
+
if (argsText.length > 0) {
|
|
254
|
+
let accepted = argsText;
|
|
255
|
+
let ok = false;
|
|
256
|
+
try {
|
|
257
|
+
JSON.parse(accepted);
|
|
258
|
+
ok = true;
|
|
259
|
+
} catch {
|
|
260
|
+
// fall through to normalization
|
|
261
|
+
}
|
|
262
|
+
if (!ok) {
|
|
263
|
+
accepted = escapeControlInsideStrings(argsText);
|
|
264
|
+
try {
|
|
265
|
+
JSON.parse(accepted);
|
|
266
|
+
ok = true;
|
|
267
|
+
} catch {
|
|
268
|
+
// still invalid
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!ok) return { calls: [], clean: text };
|
|
272
|
+
args = accepted;
|
|
273
|
+
}
|
|
274
|
+
calls.push({ id: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`, name, arguments: args });
|
|
275
|
+
}
|
|
276
|
+
return { calls, clean: head };
|
|
277
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qoder native machine identity (UMID) — the genuine SG bridge via umidpack.
|
|
3
|
+
*
|
|
4
|
+
* Research record (2026-10-30): the daily-credit campaign gates its CREDITS
|
|
5
|
+
* grant on a *genuine* Alibaba SG-produced identity. Deterministic/generated
|
|
6
|
+
* tokens do not reveal the grant on a fresh account; the `runtime-info`
|
|
7
|
+
* bridge does. Primary path therefore runs the real bridge binary from
|
|
8
|
+
* `vendor/qoder-umid/umidpack/`, auto-picked per host OS.
|
|
9
|
+
*
|
|
10
|
+
* Seed semantics (verified live): the Linux ELF resolves the user home via
|
|
11
|
+
* the passwd entry — it ignores `$HOME`/`XDG_CONFIG_HOME`, so the seed file
|
|
12
|
+
* is always `/home/<user>/.config/.locale_cfg`, one global file per Linux
|
|
13
|
+
* user. Per-account isolation therefore lives in the DATABASE (each account
|
|
14
|
+
* stores its own 350-byte seed) plus a flock-serialized bridge section:
|
|
15
|
+
* mint = wipe → run → capture seed+token; replay = write stored seed → run.
|
|
16
|
+
* Verified cycle (umidpack ELF under WSL): mint A, mint B (distinct),
|
|
17
|
+
* replay A ✓, replay B ✓, replay A again ✓.
|
|
18
|
+
*
|
|
19
|
+
* Platform behavior:
|
|
20
|
+
* win32 — the exe is hardware-bound: one identity per machine, no seed.
|
|
21
|
+
* Still a genuine SG identity (accepted by the campaign surface).
|
|
22
|
+
* linux — direct spawn of the umidpack ELF, serialized seed file.
|
|
23
|
+
* darwin — the Mach-O runs directly; seed path is the macOS user home.
|
|
24
|
+
* win32 + WSL (this repo's primary deployment) — the ELF runs via WSL with
|
|
25
|
+
* flock serialization; the bridge binary is staged into /tmp once.
|
|
26
|
+
*
|
|
27
|
+
* The generated path remains a fallback for hosts with no usable bridge: it
|
|
28
|
+
* produces well-shaped but *not* SG-signed tokens — fine for non-campaign
|
|
29
|
+
* endpoints, will not surface the daily grant.
|
|
30
|
+
*
|
|
31
|
+
* Bridge CLI (donor-verified):
|
|
32
|
+
* linux : runtime-info <env>
|
|
33
|
+
* darwin: printf '{"account":"<uid>"}\n' | runtime-info <env> --account-stdin
|
|
34
|
+
* win32 : runtime-info.exe <env> [--account-stdin]
|
|
35
|
+
*/
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
import { log } from "../../../observability/logger";
|
|
41
|
+
|
|
42
|
+
/** Valid environment codes (kept for validation/console surface). */
|
|
43
|
+
export const QODER_ENV_CODES = ["0", "3", "4", "5", "6", "7"] as const;
|
|
44
|
+
|
|
45
|
+
/** Environment code: an upstream "region/flavor", not a device identity. */
|
|
46
|
+
const ENV_CODE = "3";
|
|
47
|
+
|
|
48
|
+
/** One native identity sample (same shape the bridge prints). */
|
|
49
|
+
export interface QoderNativeIdentity {
|
|
50
|
+
readonly machineToken: string;
|
|
51
|
+
readonly machineType: string;
|
|
52
|
+
readonly machineCode: string;
|
|
53
|
+
readonly isVm: boolean;
|
|
54
|
+
readonly source: "runtime-info" | "generated";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Length of the base64url payload after the `P1gA` prefix (token total 88). */
|
|
58
|
+
const TOKEN_PAYLOAD_LENGTH = 84;
|
|
59
|
+
/** Hex length of machineType / machineCode in the bridge's output. */
|
|
60
|
+
const MACHINE_TYPE_LENGTH = 18;
|
|
61
|
+
const MACHINE_CODE_LENGTH = 18;
|
|
62
|
+
|
|
63
|
+
/** Where the umidpack binaries live, relative to the repo root. */
|
|
64
|
+
export function umidpackDir(): string {
|
|
65
|
+
return join(process.cwd(), "vendor", "qoder-umid", "umidpack");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** WSL-side staging/seed paths for the Linux bridge on a Windows host. */
|
|
69
|
+
const WSL_BIN = "/tmp/umidpack/runtime-info";
|
|
70
|
+
const WSL_SEED = "/home/risun/.config/.locale_cfg";
|
|
71
|
+
const WSL_LOCK = "/tmp/umidpack/bridge.lock";
|
|
72
|
+
|
|
73
|
+
type Bridge = { kind: "elf" | "macho" | "pe" | "wsl-elf" };
|
|
74
|
+
|
|
75
|
+
/** Picks the bridge for this host; `null` when nothing genuine can run. */
|
|
76
|
+
function pickBridge(): Bridge | null {
|
|
77
|
+
if (process.platform === "win32") {
|
|
78
|
+
// Prefer the WSL ELF (seed-capable → per-account identities); fall back
|
|
79
|
+
// to the Windows exe pair (hardware-bound, one identity per machine).
|
|
80
|
+
const elf = join(umidpackDir(), "elf-x86_64", "runtime-info");
|
|
81
|
+
if (existsSync(elf)) return { kind: "wsl-elf" };
|
|
82
|
+
const exe = join(umidpackDir(), "runtime-info.exe");
|
|
83
|
+
const dll = join(umidpackDir(), "sgsdk.dll");
|
|
84
|
+
if (existsSync(exe) && existsSync(dll)) return { kind: "pe" };
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === "darwin") {
|
|
88
|
+
const arch = process.arch === "arm64" ? "macho-arm64" : "macho-x86_64";
|
|
89
|
+
return existsSync(join(umidpackDir(), arch, "runtime-info")) ? { kind: "macho" } : null;
|
|
90
|
+
}
|
|
91
|
+
const arch = process.arch === "arm64" ? "elf-aarch64" : "elf-x86_64";
|
|
92
|
+
if (existsSync(join(umidpackDir(), arch, "runtime-info"))) return { kind: "elf" };
|
|
93
|
+
return existsSync(join(umidpackDir(), "elf-x86_64", "runtime-info")) ? { kind: "elf" } : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when a genuine bridge can run on this host. */
|
|
97
|
+
export function hasGenuineBridge(): boolean {
|
|
98
|
+
return pickBridge() !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Generated (software) identity — deterministic per account id, NOT SG-signed. */
|
|
102
|
+
export function mintQoderGeneratedIdentity(accountId: string): QoderNativeIdentity {
|
|
103
|
+
const first = createHash("sha256").update(accountId).digest("base64url");
|
|
104
|
+
const second = createHash("sha256").update(`${accountId}:${first}`).digest("base64url");
|
|
105
|
+
const token = `P1gA${`${first}${second}`.slice(0, TOKEN_PAYLOAD_LENGTH)}`;
|
|
106
|
+
return {
|
|
107
|
+
machineToken: token,
|
|
108
|
+
machineType: createHash("sha256").update(`${accountId}:type`).digest("hex").slice(0, MACHINE_TYPE_LENGTH),
|
|
109
|
+
machineCode: createHash("sha256").update(`${accountId}:code`).digest("hex").slice(0, MACHINE_CODE_LENGTH),
|
|
110
|
+
isVm: false,
|
|
111
|
+
source: "generated",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Parse one bridge JSON line into an identity; null on any mismatch. */
|
|
116
|
+
function parseBridgeLine(stdout: string): QoderNativeIdentity | null {
|
|
117
|
+
const line = stdout.split(/\r?\n/).find((l) => l.startsWith("{"));
|
|
118
|
+
if (!line) return null;
|
|
119
|
+
try {
|
|
120
|
+
const rec = JSON.parse(line) as Record<string, unknown>;
|
|
121
|
+
const machineToken = String(rec.machineToken ?? "").trim();
|
|
122
|
+
const machineType = String(rec.machineType ?? "").trim();
|
|
123
|
+
const machineCode = String(rec.machineCode ?? "").trim();
|
|
124
|
+
if (!machineToken || !machineType || !machineCode) return null;
|
|
125
|
+
const vmInfo = rec.vmInfo;
|
|
126
|
+
const isVm = typeof vmInfo === "object" && vmInfo !== null && (vmInfo as Record<string, unknown>).isVm === true;
|
|
127
|
+
return { machineToken, machineType, machineCode, isVm, source: "runtime-info" };
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Stage the ELF into the WSL filesystem once (idempotent, fast after first). */
|
|
134
|
+
function ensureWslStaged(): void {
|
|
135
|
+
const src = join(umidpackDir(), "elf-x86_64", "runtime-info").replace(/\\/g, "/");
|
|
136
|
+
const drive = /^([A-Za-z]):/.exec(src);
|
|
137
|
+
const wslSrc = drive ? `/mnt/${drive[1]!.toLowerCase()}${src.slice(2)}` : src;
|
|
138
|
+
wsl(
|
|
139
|
+
`mkdir -p /tmp/umidpack && cp "${wslSrc}" ${WSL_BIN} && chmod +x ${WSL_BIN} && ` +
|
|
140
|
+
`mkdir -p $(dirname ${WSL_SEED})`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wsl(script: string, timeout = 120_000): string {
|
|
145
|
+
const r = spawnSync("wsl", ["-e", "bash", "-lc", script], { encoding: "utf8", timeout, windowsHide: true });
|
|
146
|
+
return String(r.stdout ?? "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Run the bridge inside WSL under flock; returns the identity or null. */
|
|
150
|
+
|
|
151
|
+
function seedPath(): string {
|
|
152
|
+
if (process.platform === "win32") return WSL_SEED;
|
|
153
|
+
const home = process.env.HOME ?? "";
|
|
154
|
+
return join(home, ".config", ".locale_cfg");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mints a fresh genuine identity: wipe the seed (under the lock), run the
|
|
159
|
+
* bridge, capture the identity AND the seed it wrote. The seed is required
|
|
160
|
+
* for later replay of this same identity. Returns `null` when the bridge
|
|
161
|
+
* fails; falls back to the generated identity only when no bridge exists.
|
|
162
|
+
*/
|
|
163
|
+
export function mintQoderNativeIdentity(accountId: string): { identity: QoderNativeIdentity; seed: string } | null {
|
|
164
|
+
const bridge = pickBridge();
|
|
165
|
+
if (!bridge) {
|
|
166
|
+
log.warn("[qoder] no genuine bridge for this host — generated fallback (campaign grant will not surface)");
|
|
167
|
+
return { identity: mintQoderGeneratedIdentity(accountId), seed: "" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (bridge.kind === "wsl-elf") {
|
|
171
|
+
ensureWslStaged();
|
|
172
|
+
const out = wsl(
|
|
173
|
+
`exec 9>${WSL_LOCK}; flock 9; rm -f ${WSL_SEED}; ${WSL_BIN} ${ENV_CODE}`,
|
|
174
|
+
);
|
|
175
|
+
const identity = parseBridgeLine(out);
|
|
176
|
+
if (!identity) { log.warn("[qoder] wsl mint failed"); return null; }
|
|
177
|
+
const seed = wsl(`cat ${WSL_SEED}`);
|
|
178
|
+
return { identity, seed };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (bridge.kind === "elf") {
|
|
182
|
+
const exe = join(umidpackDir(), process.arch === "arm64" ? "elf-aarch64" : "elf-x86_64", "runtime-info");
|
|
183
|
+
const seed = seedPath();
|
|
184
|
+
const dir = seed.slice(0, seed.lastIndexOf("/"));
|
|
185
|
+
mkdirSync(dir, { recursive: true });
|
|
186
|
+
try { spawnSync("rm", ["-f", seed]); } catch { /* ignore */ }
|
|
187
|
+
const r = spawnSync(exe, [ENV_CODE], { encoding: "utf8", timeout: 25_000 });
|
|
188
|
+
const identity = parseBridgeLine(String(r.stdout ?? ""));
|
|
189
|
+
if (!identity) { log.warn("[qoder] elf mint failed"); return null; }
|
|
190
|
+
return { identity, seed: existsSync(seed) ? readFileSync(seed, "utf8") : "" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (bridge.kind === "macho") {
|
|
194
|
+
const arch = process.arch === "arm64" ? "macho-arm64" : "macho-x86_64";
|
|
195
|
+
const exe = join(umidpackDir(), arch, "runtime-info");
|
|
196
|
+
const seed = seedPath();
|
|
197
|
+
const dir = seed.slice(0, seed.lastIndexOf("/"));
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
try { spawnSync("rm", ["-f", seed]); } catch { /* ignore */ }
|
|
200
|
+
const r = spawnSync(exe, [ENV_CODE, "--account-stdin"], {
|
|
201
|
+
input: `${JSON.stringify({ account: accountId })}\n`,
|
|
202
|
+
encoding: "utf8",
|
|
203
|
+
timeout: 25_000,
|
|
204
|
+
});
|
|
205
|
+
const identity = parseBridgeLine(String(r.stdout ?? ""));
|
|
206
|
+
if (!identity) { log.warn("[qoder] macho mint failed"); return null; }
|
|
207
|
+
return { identity, seed: existsSync(seed) ? readFileSync(seed, "utf8") : "" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// pe — hardware-bound; no seed concept on Windows.
|
|
211
|
+
const exe = join(umidpackDir(), "runtime-info.exe");
|
|
212
|
+
const r = spawnSync(exe, [ENV_CODE], { encoding: "utf8", timeout: 30_000, windowsHide: true, cwd: umidpackDir() });
|
|
213
|
+
const identity = parseBridgeLine(String(r.stdout ?? ""));
|
|
214
|
+
if (!identity) { log.warn("[qoder] pe mint failed"); return null; }
|
|
215
|
+
return { identity, seed: "" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Replays an identity from a stored seed: materialize the seed at the bridge's
|
|
220
|
+
* fixed path (under the lock) and re-run — the caller verifies the token
|
|
221
|
+
* matches the stored record. Returns `null` when no seed is available, the
|
|
222
|
+
* host cannot replay (Windows exe), or the bridge failed.
|
|
223
|
+
*/
|
|
224
|
+
export function replayQoderNativeIdentity(seed: string, accountId: string): QoderNativeIdentity | null {
|
|
225
|
+
if (!seed) return null;
|
|
226
|
+
const bridge = pickBridge();
|
|
227
|
+
if (!bridge) return null;
|
|
228
|
+
|
|
229
|
+
if (bridge.kind === "wsl-elf") {
|
|
230
|
+
ensureWslStaged();
|
|
231
|
+
const quoted = seed.replace(/'/g, `'\\''`);
|
|
232
|
+
const out = wsl(
|
|
233
|
+
`exec 9>${WSL_LOCK}; flock 9; printf '%s' '${quoted}' > ${WSL_SEED}; ${WSL_BIN} ${ENV_CODE}`,
|
|
234
|
+
);
|
|
235
|
+
return parseBridgeLine(out);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (bridge.kind === "elf") {
|
|
239
|
+
const exe = join(umidpackDir(), process.arch === "arm64" ? "elf-aarch64" : "elf-x86_64", "runtime-info");
|
|
240
|
+
const seed = seedPath();
|
|
241
|
+
writeFileSync(seed, seed, "utf8");
|
|
242
|
+
const r = spawnSync(exe, [ENV_CODE], { encoding: "utf8", timeout: 25_000 });
|
|
243
|
+
return parseBridgeLine(String(r.stdout ?? ""));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (bridge.kind === "macho") {
|
|
247
|
+
const arch = process.arch === "arm64" ? "macho-arm64" : "macho-x86_64";
|
|
248
|
+
const exe = join(umidpackDir(), arch, "runtime-info");
|
|
249
|
+
const seedPathStr = seedPath();
|
|
250
|
+
writeFileSync(seedPathStr, seed, "utf8");
|
|
251
|
+
const r = spawnSync(exe, [ENV_CODE, "--account-stdin"], {
|
|
252
|
+
input: `${JSON.stringify({ account: accountId })}\n`,
|
|
253
|
+
encoding: "utf8",
|
|
254
|
+
timeout: 25_000,
|
|
255
|
+
});
|
|
256
|
+
return parseBridgeLine(String(r.stdout ?? ""));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null; // pe: cannot replay a seed on a hardware-bound identity
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Reads the current seed file content (diagnostics). */
|
|
263
|
+
export function readCurrentQoderSeed(): string {
|
|
264
|
+
const path = seedPath();
|
|
265
|
+
try {
|
|
266
|
+
if (process.platform === "win32") return wsl(`cat ${WSL_SEED} 2>/dev/null`);
|
|
267
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
268
|
+
} catch {
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The six native machine headers for one identity. */
|
|
274
|
+
export function qoderMachineHeaders(identity: QoderNativeIdentity): Record<string, string> {
|
|
275
|
+
return {
|
|
276
|
+
"cosy-machineid": identity.machineToken,
|
|
277
|
+
"cosy-machinetoken": identity.machineToken,
|
|
278
|
+
"cosy-machinetype": identity.machineType,
|
|
279
|
+
"cosy-machinecode": identity.machineCode,
|
|
280
|
+
"cosy-machineos": "x86_64_linux",
|
|
281
|
+
"cosy-machinehostname": "DESKTOP-QODER",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** For tests: force re-mint (identities are pure functions of their seed). */
|
|
286
|
+
export function resetQoderNativeIdentityCache(): void {
|
|
287
|
+
// no persistent in-process identity cache remains
|
|
288
|
+
}
|