@cleocode/core 2026.9.16 → 2026.9.17
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/dist/changesets/parser.d.ts.map +1 -1
- package/dist/changesets/parser.js +67 -0
- package/dist/changesets/parser.js.map +1 -1
- package/dist/crypto/credentials.d.ts +128 -26
- package/dist/crypto/credentials.d.ts.map +1 -1
- package/dist/crypto/credentials.js +183 -142
- package/dist/crypto/credentials.js.map +1 -1
- package/dist/docs/export-document.js +14705 -12949
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/index.d.ts +1 -1
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +1 -1
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts +1 -1
- package/dist/doctor/nexus-schema-residency.d.ts.map +1 -1
- package/dist/doctor/nexus-schema-residency.js +1 -0
- package/dist/doctor/nexus-schema-residency.js.map +1 -1
- package/dist/doctor/project-identity.d.ts +124 -0
- package/dist/doctor/project-identity.d.ts.map +1 -0
- package/dist/doctor/project-identity.js +307 -0
- package/dist/doctor/project-identity.js.map +1 -0
- package/dist/doctor/superseded-store.d.ts +8 -0
- package/dist/doctor/superseded-store.d.ts.map +1 -1
- package/dist/doctor/superseded-store.js +48 -3
- package/dist/doctor/superseded-store.js.map +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +17 -2
- package/dist/init.js.map +1 -1
- package/dist/internal.d.ts +5 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -2
- package/dist/internal.js.map +1 -1
- package/dist/llm/credentials-store.d.ts +10 -0
- package/dist/llm/credentials-store.d.ts.map +1 -1
- package/dist/llm/credentials-store.js +7 -1
- package/dist/llm/credentials-store.js.map +1 -1
- package/dist/llm/plugin-facade.js +13368 -1550
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/analyze-orchestrator.d.ts +20 -3
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +159 -10
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/assessment-store.d.ts +57 -0
- package/dist/nexus/assessment-store.d.ts.map +1 -0
- package/dist/nexus/assessment-store.js +73 -0
- package/dist/nexus/assessment-store.js.map +1 -0
- package/dist/nexus/freshness.d.ts +131 -0
- package/dist/nexus/freshness.d.ts.map +1 -0
- package/dist/nexus/freshness.js +391 -0
- package/dist/nexus/freshness.js.map +1 -0
- package/dist/nexus/graph-manifest.d.ts +88 -0
- package/dist/nexus/graph-manifest.d.ts.map +1 -0
- package/dist/nexus/graph-manifest.js +114 -0
- package/dist/nexus/graph-manifest.js.map +1 -0
- package/dist/nexus/knowledge.d.ts +21 -1
- package/dist/nexus/knowledge.d.ts.map +1 -1
- package/dist/nexus/knowledge.js +76 -37
- package/dist/nexus/knowledge.js.map +1 -1
- package/dist/nexus/path-map.d.ts +55 -0
- package/dist/nexus/path-map.d.ts.map +1 -0
- package/dist/nexus/path-map.js +87 -0
- package/dist/nexus/path-map.js.map +1 -0
- package/dist/nexus/projects-clean.d.ts +21 -28
- package/dist/nexus/projects-clean.d.ts.map +1 -1
- package/dist/nexus/projects-clean.js +168 -85
- package/dist/nexus/projects-clean.js.map +1 -1
- package/dist/nexus/query.d.ts.map +1 -1
- package/dist/nexus/query.js +3 -2
- package/dist/nexus/query.js.map +1 -1
- package/dist/nexus/registry-hygiene.d.ts +70 -0
- package/dist/nexus/registry-hygiene.d.ts.map +1 -0
- package/dist/nexus/registry-hygiene.js +113 -0
- package/dist/nexus/registry-hygiene.js.map +1 -0
- package/dist/nexus/registry.d.ts +2 -2
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +48 -12
- package/dist/nexus/registry.js.map +1 -1
- package/dist/paths.d.ts +26 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +99 -6
- package/dist/paths.js.map +1 -1
- package/dist/render/nexus/graph/index.d.ts.map +1 -1
- package/dist/render/nexus/graph/index.js +8 -0
- package/dist/render/nexus/graph/index.js.map +1 -1
- package/dist/scaffold/ensure-config.d.ts +12 -3
- package/dist/scaffold/ensure-config.d.ts.map +1 -1
- package/dist/scaffold/ensure-config.js +61 -38
- package/dist/scaffold/ensure-config.js.map +1 -1
- package/dist/scaffold/project-identity.d.ts +136 -0
- package/dist/scaffold/project-identity.d.ts.map +1 -0
- package/dist/scaffold/project-identity.js +225 -0
- package/dist/scaffold/project-identity.js.map +1 -0
- package/dist/selfimprove/fix-gen.js +13319 -1556
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/store/agent-api-key.d.ts +114 -0
- package/dist/store/agent-api-key.d.ts.map +1 -0
- package/dist/store/agent-api-key.js +136 -0
- package/dist/store/agent-api-key.js.map +1 -0
- package/dist/store/agent-registry-accessor.d.ts +4 -2
- package/dist/store/agent-registry-accessor.d.ts.map +1 -1
- package/dist/store/agent-registry-accessor.js +39 -89
- package/dist/store/agent-registry-accessor.js.map +1 -1
- package/dist/store/api-key-kdf.d.ts +7 -0
- package/dist/store/api-key-kdf.d.ts.map +1 -1
- package/dist/store/api-key-kdf.js +7 -0
- package/dist/store/api-key-kdf.js.map +1 -1
- package/dist/store/backup-crypto.d.ts +49 -0
- package/dist/store/backup-crypto.d.ts.map +1 -1
- package/dist/store/backup-crypto.js +154 -0
- package/dist/store/backup-crypto.js.map +1 -1
- package/dist/store/credential-transfer.d.ts +303 -0
- package/dist/store/credential-transfer.d.ts.map +1 -0
- package/dist/store/credential-transfer.js +774 -0
- package/dist/store/credential-transfer.js.map +1 -0
- package/dist/store/exodus/column-transforms.d.ts +79 -4
- package/dist/store/exodus/column-transforms.d.ts.map +1 -1
- package/dist/store/exodus/column-transforms.js +121 -4
- package/dist/store/exodus/column-transforms.js.map +1 -1
- package/dist/store/exodus/index.d.ts +1 -0
- package/dist/store/exodus/index.d.ts.map +1 -1
- package/dist/store/exodus/index.js +1 -0
- package/dist/store/exodus/index.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts +17 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +155 -33
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts +14 -0
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +115 -6
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/reconcile.d.ts +103 -0
- package/dist/store/exodus/reconcile.d.ts.map +1 -0
- package/dist/store/exodus/reconcile.js +728 -0
- package/dist/store/exodus/reconcile.js.map +1 -0
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +14 -2
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/store/exodus/runtime-targets.d.ts +39 -0
- package/dist/store/exodus/runtime-targets.d.ts.map +1 -0
- package/dist/store/exodus/runtime-targets.js +63 -0
- package/dist/store/exodus/runtime-targets.js.map +1 -0
- package/dist/store/exodus/table-name-map.d.ts.map +1 -1
- package/dist/store/exodus/table-name-map.js +5 -0
- package/dist/store/exodus/table-name-map.js.map +1 -1
- package/dist/store/exodus/table-order.d.ts +35 -0
- package/dist/store/exodus/table-order.d.ts.map +1 -0
- package/dist/store/exodus/table-order.js +62 -0
- package/dist/store/exodus/table-order.js.map +1 -0
- package/dist/store/exodus/verify-migration.d.ts +4 -1
- package/dist/store/exodus/verify-migration.d.ts.map +1 -1
- package/dist/store/exodus/verify-migration.js +142 -10
- package/dist/store/exodus/verify-migration.js.map +1 -1
- package/dist/store/global-salt.d.ts +16 -0
- package/dist/store/global-salt.d.ts.map +1 -1
- package/dist/store/global-salt.js +22 -7
- package/dist/store/global-salt.js.map +1 -1
- package/dist/store/legacy-tasks-lineage.d.ts +86 -0
- package/dist/store/legacy-tasks-lineage.d.ts.map +1 -0
- package/dist/store/legacy-tasks-lineage.js +285 -0
- package/dist/store/legacy-tasks-lineage.js.map +1 -0
- package/dist/store/migration-manager.d.ts +27 -0
- package/dist/store/migration-manager.d.ts.map +1 -1
- package/dist/store/migration-manager.js +63 -0
- package/dist/store/migration-manager.js.map +1 -1
- package/dist/store/portable-bundle-import.d.ts +117 -0
- package/dist/store/portable-bundle-import.d.ts.map +1 -0
- package/dist/store/portable-bundle-import.js +589 -0
- package/dist/store/portable-bundle-import.js.map +1 -0
- package/dist/store/portable-bundle-relocate.d.ts +66 -0
- package/dist/store/portable-bundle-relocate.d.ts.map +1 -0
- package/dist/store/portable-bundle-relocate.js +304 -0
- package/dist/store/portable-bundle-relocate.js.map +1 -0
- package/dist/store/portable-bundle-scan.d.ts +167 -0
- package/dist/store/portable-bundle-scan.d.ts.map +1 -0
- package/dist/store/portable-bundle-scan.js +569 -0
- package/dist/store/portable-bundle-scan.js.map +1 -0
- package/dist/store/portable-bundle.d.ts +146 -0
- package/dist/store/portable-bundle.d.ts.map +1 -0
- package/dist/store/portable-bundle.js +674 -0
- package/dist/store/portable-bundle.js.map +1 -0
- package/dist/store/regenerators.d.ts +3 -1
- package/dist/store/regenerators.d.ts.map +1 -1
- package/dist/store/regenerators.js +6 -2
- package/dist/store/regenerators.js.map +1 -1
- package/dist/store/schema/cleo-global/nexus.d.ts +98 -0
- package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
- package/dist/store/schema/cleo-global/nexus.js +19 -0
- package/dist/store/schema/cleo-global/nexus.js.map +1 -1
- package/dist/store/schema/nexus-schema.d.ts +102 -0
- package/dist/store/schema/nexus-schema.d.ts.map +1 -1
- package/dist/store/schema/nexus-schema.js +24 -0
- package/dist/store/schema/nexus-schema.js.map +1 -1
- package/dist/store/split-brain-import.d.ts +176 -0
- package/dist/store/split-brain-import.d.ts.map +1 -0
- package/dist/store/split-brain-import.js +680 -0
- package/dist/store/split-brain-import.js.map +1 -0
- package/dist/store/sqlite.d.ts +29 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +57 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +30 -5
- package/dist/system/health.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +60 -0
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +14 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +33 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/migrations/drizzle-cleo-global/20260924000000_t12354-project-paths/migration.sql +40 -0
- package/migrations/drizzle-nexus/20260924000000_t12315-nexus-parse-cache/migration.sql +21 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +3 -3
- package/templates/cleo-gitignore +7 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-api-key.ts — at-rest storage of agent API keys in
|
|
3
|
+
* `agent_registry_agents.api_key_encrypted` (global `cleo.db`).
|
|
4
|
+
*
|
|
5
|
+
* ## What went wrong (T12352)
|
|
6
|
+
*
|
|
7
|
+
* ADR-037 §5 specified the global KDF as
|
|
8
|
+
* `apiKey = HMAC-SHA256(machine-key ‖ global-salt, agentId)`. That output is an
|
|
9
|
+
* ENCRYPTION KEY: the same section says the migration "MUST decrypt each
|
|
10
|
+
* existing key … and re-encrypt using the new KDF", and the `AgentCredential`
|
|
11
|
+
* contract says the key is "stored encrypted at rest". But the variable was
|
|
12
|
+
* named `apiKey`, and T355 implemented it literally. `createProjectAgent`,
|
|
13
|
+
* `update` and `rotateKey` in `agent-registry-accessor.ts` stored the hex of
|
|
14
|
+
* the HMAC in `api_key_encrypted` and DISCARDED the real key, including the
|
|
15
|
+
* fresh key the cloud returned from `rotate-key`. The reader then returned hex
|
|
16
|
+
* of that hex as `apiKey`. Every consumer sends it as `Authorization: Bearer`
|
|
17
|
+
* to SignalDock (`cleo agent …`, `conduit` dispatch, the HTTP/SSE transports),
|
|
18
|
+
* and every one of them needs the real key.
|
|
19
|
+
*
|
|
20
|
+
* ## What is stored now
|
|
21
|
+
*
|
|
22
|
+
* `api_key_encrypted` holds `gk1:` followed by {@link encryptGlobal}
|
|
23
|
+
* ciphertext of the real key, under the global KDF ADR-037 intended, with
|
|
24
|
+
* credential id `agent:<agentId>`. It is recoverable after a restart. It moves
|
|
25
|
+
* to another device via the passphrase-sealed credential transfer
|
|
26
|
+
* (`store/credential-transfer.ts`), never by copying the machine-key.
|
|
27
|
+
*
|
|
28
|
+
* ## Rows written before the fix
|
|
29
|
+
*
|
|
30
|
+
* They hold 64 hex characters (the HMAC) or an unrecognised legacy value.
|
|
31
|
+
* Neither contains the real key, so neither can be migrated. To avoid changing
|
|
32
|
+
* behaviour for anything that depends on today's value, they are read back
|
|
33
|
+
* exactly as before, but flagged `requiresReauth` with the command that
|
|
34
|
+
* re-registers the agent's real key.
|
|
35
|
+
*
|
|
36
|
+
* @task T12352
|
|
37
|
+
* @module store/agent-api-key
|
|
38
|
+
*/
|
|
39
|
+
import { type CredentialKeyOptions } from '../crypto/credentials.js';
|
|
40
|
+
/** Prefix marking an `api_key_encrypted` value as a T12352 global-KDF ciphertext. */
|
|
41
|
+
export declare const AGENT_KEY_CIPHERTEXT_PREFIX = "gk1:";
|
|
42
|
+
/**
|
|
43
|
+
* The global-KDF credential id for an agent's API key. The `agent:` prefix
|
|
44
|
+
* separates agent keys from other global credentials (`service:<p>:<l>`).
|
|
45
|
+
*
|
|
46
|
+
* @param agentId - Agent business identifier.
|
|
47
|
+
* @returns The id passed to {@link encryptGlobal} / {@link decryptGlobal}.
|
|
48
|
+
* @task T12352
|
|
49
|
+
*/
|
|
50
|
+
export declare function agentKeyCredentialId(agentId: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* How an `api_key_encrypted` value was written.
|
|
53
|
+
*
|
|
54
|
+
* - `none` — empty / NULL.
|
|
55
|
+
* - `ciphertext` — a T12352 `gk1:` global-KDF ciphertext (recoverable).
|
|
56
|
+
* - `legacy-derived` — the pre-T12352 hex HMAC (the real key was discarded).
|
|
57
|
+
* - `legacy-unknown` — anything else (e.g. a pre-T310 path-KDF ciphertext).
|
|
58
|
+
*
|
|
59
|
+
* @task T12352
|
|
60
|
+
*/
|
|
61
|
+
export type StoredAgentKeyKind = 'none' | 'ciphertext' | 'legacy-derived' | 'legacy-unknown';
|
|
62
|
+
/**
|
|
63
|
+
* Classify a stored `api_key_encrypted` value without decrypting it.
|
|
64
|
+
*
|
|
65
|
+
* @param stored - Raw column value.
|
|
66
|
+
* @returns Its kind.
|
|
67
|
+
* @task T12352
|
|
68
|
+
*/
|
|
69
|
+
export declare function classifyStoredAgentKey(stored: string | null | undefined): StoredAgentKeyKind;
|
|
70
|
+
/**
|
|
71
|
+
* Encrypt an agent API key for `api_key_encrypted`.
|
|
72
|
+
*
|
|
73
|
+
* @param apiKey - The real key (e.g. `sk_live_…`). Empty yields `null`.
|
|
74
|
+
* @param agentId - Agent business identifier (binds the key derivation).
|
|
75
|
+
* @param options - Which CLEO home's machine-key + salt (defaults to ambient).
|
|
76
|
+
* @returns `gk1:<ciphertext>`, or `null` when there is no key to store.
|
|
77
|
+
* @task T12352
|
|
78
|
+
*/
|
|
79
|
+
export declare function sealAgentApiKey(apiKey: string, agentId: string, options?: CredentialKeyOptions): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Result of reading a stored agent API key.
|
|
82
|
+
*
|
|
83
|
+
* @task T12352
|
|
84
|
+
*/
|
|
85
|
+
export interface AgentApiKeyRead {
|
|
86
|
+
/** The value to expose as `AgentCredential.apiKey`. */
|
|
87
|
+
readonly apiKey: string;
|
|
88
|
+
/** Kind of the stored value. */
|
|
89
|
+
readonly kind: StoredAgentKeyKind;
|
|
90
|
+
/**
|
|
91
|
+
* True when the real key is not recoverable from storage and must be
|
|
92
|
+
* re-registered: legacy values, or a ciphertext this device cannot decrypt.
|
|
93
|
+
*/
|
|
94
|
+
readonly requiresReauth: boolean;
|
|
95
|
+
/** Why re-registration is needed (absent when not). */
|
|
96
|
+
readonly reason?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read an agent API key from `api_key_encrypted`.
|
|
100
|
+
*
|
|
101
|
+
* A `gk1:` ciphertext is decrypted to the real key. Legacy values are returned
|
|
102
|
+
* exactly as the pre-T12352 reader returned them, so nothing that works today
|
|
103
|
+
* changes, but they are flagged `requiresReauth`. A ciphertext that does not
|
|
104
|
+
* decrypt here (another device's key) is flagged too; it never throws, because
|
|
105
|
+
* a listing must not fail on one bad row.
|
|
106
|
+
*
|
|
107
|
+
* @param stored - Raw column value.
|
|
108
|
+
* @param agentId - Agent business identifier.
|
|
109
|
+
* @param options - Which CLEO home's machine-key + salt (defaults to ambient).
|
|
110
|
+
* @returns The key to expose, its kind, and whether re-registration is needed.
|
|
111
|
+
* @task T12352
|
|
112
|
+
*/
|
|
113
|
+
export declare function openAgentApiKey(stored: string | null | undefined, agentId: string, options?: CredentialKeyOptions): Promise<AgentApiKeyRead>;
|
|
114
|
+
//# sourceMappingURL=agent-api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-api-key.d.ts","sourceRoot":"","sources":["../../src/store/agent-api-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,KAAK,oBAAoB,EAAgC,MAAM,0BAA0B,CAAC;AAEnG,qFAAqF;AACrF,eAAO,MAAM,2BAA2B,SAAS,CAAC;AAKlD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,YAAY,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAE7F;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB,CAK5F;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CA+B1B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-api-key.ts — at-rest storage of agent API keys in
|
|
3
|
+
* `agent_registry_agents.api_key_encrypted` (global `cleo.db`).
|
|
4
|
+
*
|
|
5
|
+
* ## What went wrong (T12352)
|
|
6
|
+
*
|
|
7
|
+
* ADR-037 §5 specified the global KDF as
|
|
8
|
+
* `apiKey = HMAC-SHA256(machine-key ‖ global-salt, agentId)`. That output is an
|
|
9
|
+
* ENCRYPTION KEY: the same section says the migration "MUST decrypt each
|
|
10
|
+
* existing key … and re-encrypt using the new KDF", and the `AgentCredential`
|
|
11
|
+
* contract says the key is "stored encrypted at rest". But the variable was
|
|
12
|
+
* named `apiKey`, and T355 implemented it literally. `createProjectAgent`,
|
|
13
|
+
* `update` and `rotateKey` in `agent-registry-accessor.ts` stored the hex of
|
|
14
|
+
* the HMAC in `api_key_encrypted` and DISCARDED the real key, including the
|
|
15
|
+
* fresh key the cloud returned from `rotate-key`. The reader then returned hex
|
|
16
|
+
* of that hex as `apiKey`. Every consumer sends it as `Authorization: Bearer`
|
|
17
|
+
* to SignalDock (`cleo agent …`, `conduit` dispatch, the HTTP/SSE transports),
|
|
18
|
+
* and every one of them needs the real key.
|
|
19
|
+
*
|
|
20
|
+
* ## What is stored now
|
|
21
|
+
*
|
|
22
|
+
* `api_key_encrypted` holds `gk1:` followed by {@link encryptGlobal}
|
|
23
|
+
* ciphertext of the real key, under the global KDF ADR-037 intended, with
|
|
24
|
+
* credential id `agent:<agentId>`. It is recoverable after a restart. It moves
|
|
25
|
+
* to another device via the passphrase-sealed credential transfer
|
|
26
|
+
* (`store/credential-transfer.ts`), never by copying the machine-key.
|
|
27
|
+
*
|
|
28
|
+
* ## Rows written before the fix
|
|
29
|
+
*
|
|
30
|
+
* They hold 64 hex characters (the HMAC) or an unrecognised legacy value.
|
|
31
|
+
* Neither contains the real key, so neither can be migrated. To avoid changing
|
|
32
|
+
* behaviour for anything that depends on today's value, they are read back
|
|
33
|
+
* exactly as before, but flagged `requiresReauth` with the command that
|
|
34
|
+
* re-registers the agent's real key.
|
|
35
|
+
*
|
|
36
|
+
* @task T12352
|
|
37
|
+
* @module store/agent-api-key
|
|
38
|
+
*/
|
|
39
|
+
import { decryptGlobal, encryptGlobal } from '../crypto/credentials.js';
|
|
40
|
+
/** Prefix marking an `api_key_encrypted` value as a T12352 global-KDF ciphertext. */
|
|
41
|
+
export const AGENT_KEY_CIPHERTEXT_PREFIX = 'gk1:';
|
|
42
|
+
/** Shape of a pre-T12352 value: the hex HMAC the old code stored instead of the key. */
|
|
43
|
+
const LEGACY_DERIVED_PATTERN = /^[0-9a-f]{64}$/;
|
|
44
|
+
/**
|
|
45
|
+
* The global-KDF credential id for an agent's API key. The `agent:` prefix
|
|
46
|
+
* separates agent keys from other global credentials (`service:<p>:<l>`).
|
|
47
|
+
*
|
|
48
|
+
* @param agentId - Agent business identifier.
|
|
49
|
+
* @returns The id passed to {@link encryptGlobal} / {@link decryptGlobal}.
|
|
50
|
+
* @task T12352
|
|
51
|
+
*/
|
|
52
|
+
export function agentKeyCredentialId(agentId) {
|
|
53
|
+
return `agent:${agentId}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Classify a stored `api_key_encrypted` value without decrypting it.
|
|
57
|
+
*
|
|
58
|
+
* @param stored - Raw column value.
|
|
59
|
+
* @returns Its kind.
|
|
60
|
+
* @task T12352
|
|
61
|
+
*/
|
|
62
|
+
export function classifyStoredAgentKey(stored) {
|
|
63
|
+
if (stored === null || stored === undefined || stored === '')
|
|
64
|
+
return 'none';
|
|
65
|
+
if (stored.startsWith(AGENT_KEY_CIPHERTEXT_PREFIX))
|
|
66
|
+
return 'ciphertext';
|
|
67
|
+
if (LEGACY_DERIVED_PATTERN.test(stored))
|
|
68
|
+
return 'legacy-derived';
|
|
69
|
+
return 'legacy-unknown';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Encrypt an agent API key for `api_key_encrypted`.
|
|
73
|
+
*
|
|
74
|
+
* @param apiKey - The real key (e.g. `sk_live_…`). Empty yields `null`.
|
|
75
|
+
* @param agentId - Agent business identifier (binds the key derivation).
|
|
76
|
+
* @param options - Which CLEO home's machine-key + salt (defaults to ambient).
|
|
77
|
+
* @returns `gk1:<ciphertext>`, or `null` when there is no key to store.
|
|
78
|
+
* @task T12352
|
|
79
|
+
*/
|
|
80
|
+
export async function sealAgentApiKey(apiKey, agentId, options = {}) {
|
|
81
|
+
if (apiKey.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
return (AGENT_KEY_CIPHERTEXT_PREFIX +
|
|
84
|
+
(await encryptGlobal(apiKey, agentKeyCredentialId(agentId), options)));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The value pre-T12352 readers returned for a stored string: its UTF-8 bytes
|
|
88
|
+
* as hex. Kept for legacy rows so their observable `apiKey` does not change.
|
|
89
|
+
*/
|
|
90
|
+
function legacyReadValue(stored) {
|
|
91
|
+
return Buffer.from(stored).toString('hex');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read an agent API key from `api_key_encrypted`.
|
|
95
|
+
*
|
|
96
|
+
* A `gk1:` ciphertext is decrypted to the real key. Legacy values are returned
|
|
97
|
+
* exactly as the pre-T12352 reader returned them, so nothing that works today
|
|
98
|
+
* changes, but they are flagged `requiresReauth`. A ciphertext that does not
|
|
99
|
+
* decrypt here (another device's key) is flagged too; it never throws, because
|
|
100
|
+
* a listing must not fail on one bad row.
|
|
101
|
+
*
|
|
102
|
+
* @param stored - Raw column value.
|
|
103
|
+
* @param agentId - Agent business identifier.
|
|
104
|
+
* @param options - Which CLEO home's machine-key + salt (defaults to ambient).
|
|
105
|
+
* @returns The key to expose, its kind, and whether re-registration is needed.
|
|
106
|
+
* @task T12352
|
|
107
|
+
*/
|
|
108
|
+
export async function openAgentApiKey(stored, agentId, options = {}) {
|
|
109
|
+
const kind = classifyStoredAgentKey(stored);
|
|
110
|
+
if (kind === 'none' || stored === null || stored === undefined) {
|
|
111
|
+
return { apiKey: '', kind, requiresReauth: false };
|
|
112
|
+
}
|
|
113
|
+
if (kind === 'ciphertext') {
|
|
114
|
+
try {
|
|
115
|
+
const apiKey = await decryptGlobal(stored.slice(AGENT_KEY_CIPHERTEXT_PREFIX.length), agentKeyCredentialId(agentId), options);
|
|
116
|
+
return { apiKey, kind, requiresReauth: false };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
return {
|
|
120
|
+
apiKey: '',
|
|
121
|
+
kind,
|
|
122
|
+
requiresReauth: true,
|
|
123
|
+
reason: `stored key does not decrypt on this device: ${err instanceof Error ? err.message : String(err)}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
apiKey: legacyReadValue(stored),
|
|
129
|
+
kind,
|
|
130
|
+
requiresReauth: true,
|
|
131
|
+
reason: kind === 'legacy-derived'
|
|
132
|
+
? 'stored before T12352: the column holds a derived HMAC, not the API key, which was discarded'
|
|
133
|
+
: 'stored in an unrecognised legacy format that does not contain a recoverable API key',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=agent-api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-api-key.js","sourceRoot":"","sources":["../../src/store/agent-api-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAA6B,aAAa,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEnG,qFAAqF;AACrF,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD,wFAAwF;AACxF,MAAM,sBAAsB,GAAG,gBAAgB,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,OAAO,SAAS,OAAO,EAAE,CAAC;AAC5B,CAAC;AAcD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAiC;IACtE,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,MAAM,CAAC;IAC5E,IAAI,MAAM,CAAC,UAAU,CAAC,2BAA2B,CAAC;QAAE,OAAO,YAAY,CAAC;IACxE,IAAI,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACjE,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,OAAe,EACf,UAAgC,EAAE;IAElC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,CACL,2BAA2B;QAC3B,CAAC,MAAM,aAAa,CAAC,MAAM,EAAE,oBAAoB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CACtE,CAAC;AACJ,CAAC;AAqBD;;;GAGG;AACH,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAiC,EACjC,OAAe,EACf,UAAgC,EAAE;IAElC,MAAM,IAAI,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,IAAI,KAAK,MAAM,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/D,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAChC,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,MAAM,CAAC,EAChD,oBAAoB,CAAC,OAAO,CAAC,EAC7B,OAAO,CACR,CAAC;YACF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,MAAM,EAAE,EAAE;gBACV,IAAI;gBACJ,cAAc,EAAE,IAAI;gBACpB,MAAM,EAAE,+CAA+C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;aAC1G,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO;QACL,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC;QAC/B,IAAI;QACJ,cAAc,EAAE,IAAI;QACpB,MAAM,EACJ,IAAI,KAAK,gBAAgB;YACvB,CAAC,CAAC,6FAA6F;YAC/F,CAAC,CAAC,qFAAqF;KAC5F,CAAC;AACJ,CAAC"}
|
|
@@ -174,7 +174,9 @@ export declare function listAgentsForProject(projectRoot: string, opts?: {
|
|
|
174
174
|
* Write order: global first, then project ref. If the project ref write fails,
|
|
175
175
|
* the global row remains (recoverable via `cleo agent attach <id>`).
|
|
176
176
|
*
|
|
177
|
-
* API key
|
|
177
|
+
* API key storage (T12352): the real `spec.apiKey` is stored encrypted under the
|
|
178
|
+
* global KDF (ADR-037 §5) — see `agent-api-key.ts`. An empty key on re-register
|
|
179
|
+
* keeps the stored one.
|
|
178
180
|
*
|
|
179
181
|
* @param projectRoot - Absolute path to the project root directory.
|
|
180
182
|
* @param spec - Agent creation spec (without createdAt/updatedAt).
|
|
@@ -338,7 +340,7 @@ export declare class AgentRegistryAccessor implements AgentRegistryAPI {
|
|
|
338
340
|
force?: boolean;
|
|
339
341
|
}): Promise<void>;
|
|
340
342
|
/**
|
|
341
|
-
* Rotate API key via cloud endpoint and
|
|
343
|
+
* Rotate API key via cloud endpoint and store the NEW key encrypted (T12352)
|
|
342
344
|
* in global cleo.db (Agent Registry).
|
|
343
345
|
*
|
|
344
346
|
* @param agentId - Agent business identifier.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-registry-accessor.d.ts","sourceRoot":"","sources":["../../src/store/agent-registry-accessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;
|
|
1
|
+
{"version":3,"file":"agent-registry-accessor.d.ts","sourceRoot":"","sources":["../../src/store/agent-registry-accessor.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,15 +42,10 @@
|
|
|
42
42
|
* @epic T310
|
|
43
43
|
* @epic T11249
|
|
44
44
|
*/
|
|
45
|
-
import { randomBytes } from 'node:crypto';
|
|
46
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
47
45
|
import { createRequire } from 'node:module';
|
|
48
|
-
import {
|
|
49
|
-
import { getCleoHome } from '../paths.js';
|
|
46
|
+
import { openAgentApiKey, sealAgentApiKey } from './agent-api-key.js';
|
|
50
47
|
import { ensureGlobalAgentRegistryDb, getGlobalAgentRegistryNativeDb, } from './agent-registry-store.js';
|
|
51
|
-
import { deriveApiKey } from './api-key-kdf.js';
|
|
52
48
|
import { ensureConduitDb, getConduitDbPath } from './conduit-sqlite.js';
|
|
53
|
-
import { getGlobalSalt } from './global-salt.js';
|
|
54
49
|
import { applyPerfPragmas } from './sqlite-pragmas.js';
|
|
55
50
|
// ---------------------------------------------------------------------------
|
|
56
51
|
// node:sqlite interop (createRequire for ESM / Vitest compat)
|
|
@@ -58,45 +53,6 @@ import { applyPerfPragmas } from './sqlite-pragmas.js';
|
|
|
58
53
|
const _require = createRequire(import.meta.url);
|
|
59
54
|
const { DatabaseSync } = _require('node:sqlite');
|
|
60
55
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// Machine-key helper (internal — mirrors credentials.ts private getMachineKey)
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
/** Machine-key constants. */
|
|
64
|
-
const MACHINE_KEY_LENGTH = 32;
|
|
65
|
-
/**
|
|
66
|
-
* Read or auto-generate the machine key (32 bytes).
|
|
67
|
-
* Machine key lives at `getCleoHome()/machine-key` (same XDG root as the global salt).
|
|
68
|
-
*
|
|
69
|
-
* @returns A 32-byte Buffer.
|
|
70
|
-
* @task T355
|
|
71
|
-
* @epic T310
|
|
72
|
-
*/
|
|
73
|
-
function readMachineKey() {
|
|
74
|
-
const keyPath = join(getCleoHome(), 'machine-key');
|
|
75
|
-
if (!existsSync(keyPath)) {
|
|
76
|
-
const cleoHome = getCleoHome();
|
|
77
|
-
if (!existsSync(cleoHome)) {
|
|
78
|
-
mkdirSync(cleoHome, { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
const key = randomBytes(MACHINE_KEY_LENGTH);
|
|
81
|
-
writeFileSync(keyPath, key, { mode: 0o600 });
|
|
82
|
-
return key;
|
|
83
|
-
}
|
|
84
|
-
// Validate permissions on POSIX
|
|
85
|
-
if (process.platform !== 'win32') {
|
|
86
|
-
const stat = statSync(keyPath);
|
|
87
|
-
const mode = stat.mode & 0o777;
|
|
88
|
-
if (mode !== 0o600) {
|
|
89
|
-
throw new Error(`Machine key at ${keyPath} has wrong permissions: expected 0o600, got 0o${mode.toString(8)}. ` +
|
|
90
|
-
`Fix with: chmod 600 ${keyPath}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const key = readFileSync(keyPath);
|
|
94
|
-
if (key.length !== MACHINE_KEY_LENGTH) {
|
|
95
|
-
throw new Error(`Machine key at ${keyPath} has wrong length: expected ${MACHINE_KEY_LENGTH} bytes, got ${key.length}.`);
|
|
96
|
-
}
|
|
97
|
-
return key;
|
|
98
|
-
}
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
56
|
// Row-to-type converters
|
|
101
57
|
// ---------------------------------------------------------------------------
|
|
102
58
|
/**
|
|
@@ -118,24 +74,26 @@ function rowToProjectRef(row) {
|
|
|
118
74
|
};
|
|
119
75
|
}
|
|
120
76
|
/**
|
|
121
|
-
* Convert a global cleo.db:agent_registry_agents row to an `AgentCredential
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
77
|
+
* Convert a global cleo.db:agent_registry_agents row to an `AgentCredential`,
|
|
78
|
+
* decrypting the stored API key (T12352).
|
|
79
|
+
*
|
|
80
|
+
* `api_key_encrypted` holds a `gk1:` global-KDF ciphertext of the real key.
|
|
81
|
+
* Rows written before T12352 hold a derived HMAC instead (the real key was
|
|
82
|
+
* discarded); they read back exactly as before and carry `requiresReauth`.
|
|
125
83
|
*
|
|
126
84
|
* @param row - Raw SQLite row from global cleo.db:agent_registry_agents.
|
|
127
|
-
* @returns Typed `AgentCredential
|
|
85
|
+
* @returns Typed `AgentCredential`.
|
|
128
86
|
* @task T355
|
|
87
|
+
* @task T12352
|
|
129
88
|
* @epic T310
|
|
130
89
|
*/
|
|
131
|
-
function rowToCredential(row) {
|
|
90
|
+
async function rowToCredential(row) {
|
|
91
|
+
const key = await openAgentApiKey(row.api_key_encrypted, row.agent_id);
|
|
132
92
|
return {
|
|
133
93
|
agentId: row.agent_id,
|
|
134
94
|
displayName: row.name,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// The reauth flow in `cleo agent auth` handles re-keying (T358).
|
|
138
|
-
apiKey: row.api_key_encrypted ? Buffer.from(row.api_key_encrypted).toString('hex') : '',
|
|
95
|
+
apiKey: key.apiKey,
|
|
96
|
+
...(key.requiresReauth ? { requiresReauth: true } : {}),
|
|
139
97
|
apiBaseUrl: row.api_base_url,
|
|
140
98
|
classification: row.classification ?? undefined,
|
|
141
99
|
privacyTier: row.privacy_tier,
|
|
@@ -239,9 +197,9 @@ export function rowToResolvedAgent(row) {
|
|
|
239
197
|
* @task T355
|
|
240
198
|
* @epic T310
|
|
241
199
|
*/
|
|
242
|
-
function mergeToAgentWithOverride(agentRow, refRow) {
|
|
200
|
+
async function mergeToAgentWithOverride(agentRow, refRow) {
|
|
243
201
|
return {
|
|
244
|
-
...rowToCredential(agentRow),
|
|
202
|
+
...(await rowToCredential(agentRow)),
|
|
245
203
|
projectRef: refRow ? rowToProjectRef(refRow) : null,
|
|
246
204
|
};
|
|
247
205
|
}
|
|
@@ -397,11 +355,11 @@ export async function lookupAgent(projectRoot, agentId, opts) {
|
|
|
397
355
|
// INNER JOIN semantics: must have a project ref with enabled=1
|
|
398
356
|
if (!refRow || refRow.enabled === 0)
|
|
399
357
|
return null;
|
|
400
|
-
return mergeToAgentWithOverride(agentRow, refRow);
|
|
358
|
+
return await mergeToAgentWithOverride(agentRow, refRow);
|
|
401
359
|
}
|
|
402
360
|
// includeGlobal=true: return global agent; populate projectRef only when enabled=1
|
|
403
361
|
const effectiveRef = refRow && refRow.enabled === 1 ? refRow : null;
|
|
404
|
-
return mergeToAgentWithOverride(agentRow, effectiveRef);
|
|
362
|
+
return await mergeToAgentWithOverride(agentRow, effectiveRef);
|
|
405
363
|
}
|
|
406
364
|
finally {
|
|
407
365
|
// NOTE: globalDb is the SHARED dual-scope handle — never close it (T11562).
|
|
@@ -464,7 +422,7 @@ export async function listAgentsForProject(projectRoot, opts) {
|
|
|
464
422
|
if (includeGlobal) {
|
|
465
423
|
// Return all global agents; populate projectRef only for attached ones
|
|
466
424
|
const effectiveRef = ref && ref.enabled === 1 ? ref : null;
|
|
467
|
-
result.push(mergeToAgentWithOverride(agentRow, effectiveRef));
|
|
425
|
+
result.push(await mergeToAgentWithOverride(agentRow, effectiveRef));
|
|
468
426
|
}
|
|
469
427
|
else {
|
|
470
428
|
// INNER JOIN: only agents with a project ref row
|
|
@@ -472,7 +430,7 @@ export async function listAgentsForProject(projectRoot, opts) {
|
|
|
472
430
|
continue;
|
|
473
431
|
if (!includeDisabled && ref.enabled === 0)
|
|
474
432
|
continue;
|
|
475
|
-
result.push(mergeToAgentWithOverride(agentRow, ref));
|
|
433
|
+
result.push(await mergeToAgentWithOverride(agentRow, ref));
|
|
476
434
|
}
|
|
477
435
|
}
|
|
478
436
|
return result;
|
|
@@ -489,7 +447,9 @@ export async function listAgentsForProject(projectRoot, opts) {
|
|
|
489
447
|
* Write order: global first, then project ref. If the project ref write fails,
|
|
490
448
|
* the global row remains (recoverable via `cleo agent attach <id>`).
|
|
491
449
|
*
|
|
492
|
-
* API key
|
|
450
|
+
* API key storage (T12352): the real `spec.apiKey` is stored encrypted under the
|
|
451
|
+
* global KDF (ADR-037 §5) — see `agent-api-key.ts`. An empty key on re-register
|
|
452
|
+
* keeps the stored one.
|
|
493
453
|
*
|
|
494
454
|
* @param projectRoot - Absolute path to the project root directory.
|
|
495
455
|
* @param spec - Agent creation spec (without createdAt/updatedAt).
|
|
@@ -506,16 +466,9 @@ export async function createProjectAgent(projectRoot, spec) {
|
|
|
506
466
|
await ensureConduitDb(projectRoot);
|
|
507
467
|
const nowTs = Math.floor(Date.now() / 1000);
|
|
508
468
|
const nowIso = new Date(nowTs * 1000).toISOString();
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
const derivedKey = deriveApiKey({
|
|
513
|
-
machineKey,
|
|
514
|
-
globalSalt,
|
|
515
|
-
agentId: spec.agentId,
|
|
516
|
-
});
|
|
517
|
-
// Store as hex string in the encrypted column
|
|
518
|
-
const apiKeyEncrypted = derivedKey.toString('hex');
|
|
469
|
+
// T12352: store the REAL key, encrypted under the global KDF (ADR-037 §5).
|
|
470
|
+
// null = no key supplied: a re-register keeps the stored key.
|
|
471
|
+
const apiKeyEncrypted = await sealAgentApiKey(spec.apiKey, spec.agentId);
|
|
519
472
|
// SHARED global cleo.db handle — do NOT close (lifecycle owned by openDualScopeDb).
|
|
520
473
|
const globalDb = await openGlobalDb();
|
|
521
474
|
{
|
|
@@ -540,9 +493,11 @@ export async function createProjectAgent(projectRoot, spec) {
|
|
|
540
493
|
// Update identity in global DB (idempotent re-register)
|
|
541
494
|
globalDb
|
|
542
495
|
.prepare(`UPDATE agent_registry_agents SET name = ?, class = ?, privacy_tier = ?, capabilities = ?, skills = ?,
|
|
543
|
-
transport_type = ?, api_key_encrypted = ?,
|
|
496
|
+
transport_type = ?, api_key_encrypted = COALESCE(?, api_key_encrypted),
|
|
497
|
+
requires_reauth = CASE WHEN ? IS NULL THEN requires_reauth ELSE 0 END,
|
|
498
|
+
api_base_url = ?, classification = ?,
|
|
544
499
|
transport_config = ?, is_active = ?, updated_at = ? WHERE agent_id = ?`)
|
|
545
|
-
.run(spec.displayName, spec.classification ?? 'custom', spec.privacyTier, JSON.stringify(spec.capabilities), JSON.stringify(spec.skills), spec.transportType ?? 'http', apiKeyEncrypted, spec.apiBaseUrl, spec.classification ?? null, JSON.stringify(spec.transportConfig), spec.isActive ? 1 : 0, nowIso, spec.agentId);
|
|
500
|
+
.run(spec.displayName, spec.classification ?? 'custom', spec.privacyTier, JSON.stringify(spec.capabilities), JSON.stringify(spec.skills), spec.transportType ?? 'http', apiKeyEncrypted, apiKeyEncrypted, spec.apiBaseUrl, spec.classification ?? null, JSON.stringify(spec.transportConfig), spec.isActive ? 1 : 0, nowIso, spec.agentId);
|
|
546
501
|
syncJunctionTables(globalDb, agentUuid, spec.capabilities, spec.skills);
|
|
547
502
|
}
|
|
548
503
|
}
|
|
@@ -779,7 +734,7 @@ export class AgentRegistryAccessor {
|
|
|
779
734
|
: globalDb
|
|
780
735
|
.prepare('SELECT * FROM agent_registry_agents ORDER BY name ASC')
|
|
781
736
|
.all();
|
|
782
|
-
return rows.map(rowToCredential);
|
|
737
|
+
return Promise.all(rows.map(rowToCredential));
|
|
783
738
|
}
|
|
784
739
|
/**
|
|
785
740
|
* Update agent identity fields in global cleo.db (Agent Registry).
|
|
@@ -841,12 +796,9 @@ export class AgentRegistryAccessor {
|
|
|
841
796
|
params.push(updates.isActive ? 1 : 0);
|
|
842
797
|
}
|
|
843
798
|
if (updates.apiKey !== undefined) {
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const derivedKey = deriveApiKey({ machineKey, globalSalt, agentId });
|
|
848
|
-
sets.push('api_key_encrypted = ?');
|
|
849
|
-
params.push(derivedKey.toString('hex'));
|
|
799
|
+
// T12352: store the supplied key itself, encrypted (was: a derived HMAC).
|
|
800
|
+
sets.push('api_key_encrypted = ?', 'requires_reauth = 0');
|
|
801
|
+
params.push(await sealAgentApiKey(updates.apiKey, agentId));
|
|
850
802
|
}
|
|
851
803
|
params.push(agentId);
|
|
852
804
|
globalDb
|
|
@@ -933,7 +885,7 @@ export class AgentRegistryAccessor {
|
|
|
933
885
|
// NOTE: globalDb is the SHARED dual-scope handle — never close it (T11562).
|
|
934
886
|
}
|
|
935
887
|
/**
|
|
936
|
-
* Rotate API key via cloud endpoint and
|
|
888
|
+
* Rotate API key via cloud endpoint and store the NEW key encrypted (T12352)
|
|
937
889
|
* in global cleo.db (Agent Registry).
|
|
938
890
|
*
|
|
939
891
|
* @param agentId - Agent business identifier.
|
|
@@ -960,17 +912,15 @@ export class AgentRegistryAccessor {
|
|
|
960
912
|
const newApiKey = data.data?.apiKey;
|
|
961
913
|
if (!newApiKey)
|
|
962
914
|
throw new Error('Cloud API did not return a new API key');
|
|
963
|
-
//
|
|
964
|
-
const
|
|
965
|
-
const globalSalt = getGlobalSalt();
|
|
966
|
-
const derivedKey = deriveApiKey({ machineKey, globalSalt, agentId });
|
|
915
|
+
// T12352: keep the key the cloud just issued (it was discarded before).
|
|
916
|
+
const sealedKey = await sealAgentApiKey(newApiKey, agentId);
|
|
967
917
|
// T11622 cutover: `updated_at` is TEXT ISO-8601 (GLOB CHECK).
|
|
968
918
|
const nowIso = new Date().toISOString();
|
|
969
919
|
// SHARED global cleo.db handle — do NOT close (lifecycle owned by openDualScopeDb).
|
|
970
920
|
const globalDb = await openGlobalDb();
|
|
971
921
|
globalDb
|
|
972
922
|
.prepare('UPDATE agent_registry_agents SET api_key_encrypted = ?, updated_at = ?, requires_reauth = 0 WHERE agent_id = ?')
|
|
973
|
-
.run(
|
|
923
|
+
.run(sealedKey, nowIso, agentId);
|
|
974
924
|
// NOTE: globalDb is the SHARED dual-scope handle — never close it (T11562).
|
|
975
925
|
return { agentId, newApiKey: `${newApiKey.substring(0, 8)}...rotated` };
|
|
976
926
|
}
|
|
@@ -996,7 +946,7 @@ export class AgentRegistryAccessor {
|
|
|
996
946
|
.prepare('SELECT * FROM agent_registry_agents WHERE agent_id = ? AND is_active = 1')
|
|
997
947
|
.get(ref.agent_id);
|
|
998
948
|
if (agentRow)
|
|
999
|
-
return rowToCredential(agentRow);
|
|
949
|
+
return await rowToCredential(agentRow);
|
|
1000
950
|
}
|
|
1001
951
|
// Fall back to global last_used_at if no project-local activity recorded
|
|
1002
952
|
const row = globalDb
|
|
@@ -1004,7 +954,7 @@ export class AgentRegistryAccessor {
|
|
|
1004
954
|
.get();
|
|
1005
955
|
if (!row)
|
|
1006
956
|
return null;
|
|
1007
|
-
return rowToCredential(row);
|
|
957
|
+
return await rowToCredential(row);
|
|
1008
958
|
}
|
|
1009
959
|
finally {
|
|
1010
960
|
// NOTE: globalDb is the SHARED dual-scope handle — never close it (T11562).
|