@openpalm/lib 0.10.2 → 0.11.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.md +2 -2
- package/package.json +7 -3
- package/src/control-plane/admin-token.ts +73 -0
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +307 -0
- package/src/control-plane/channels.ts +3 -3
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -24
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +103 -65
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +54 -57
- package/src/control-plane/docker.ts +55 -21
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +80 -0
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +260 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +187 -289
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +34 -65
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/paths.ts +82 -0
- package/src/control-plane/provider-config.ts +2 -2
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +105 -27
- package/src/control-plane/registry.test.ts +49 -47
- package/src/control-plane/registry.ts +71 -50
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-backend.test.ts +98 -111
- package/src/control-plane/secret-backend.ts +221 -181
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +93 -51
- package/src/control-plane/setup-config.schema.json +5 -17
- package/src/control-plane/setup-status.ts +9 -29
- package/src/control-plane/setup-validation.ts +23 -23
- package/src/control-plane/setup.test.ts +138 -239
- package/src/control-plane/setup.ts +215 -130
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +59 -58
- package/src/control-plane/spec-to-env.ts +52 -142
- package/src/control-plane/spec-validator.ts +2 -99
- package/src/control-plane/stack-spec.test.ts +21 -77
- package/src/control-plane/stack-spec.ts +7 -83
- package/src/control-plane/types.ts +12 -28
- package/src/control-plane/ui-assets.ts +349 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +86 -48
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/audit.ts +0 -40
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/redact-schema.ts +0 -50
|
@@ -1,61 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime configuration validation for the OpenPalm control plane.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Validation is a presence check on the canonical env keys we expect in
|
|
5
|
+
* the live config/stack/stack.env file. The
|
|
6
|
+
* historical schema files and external validation binary were retired in
|
|
7
|
+
* #391; everything advisory is surfaced as a non-blocking warning. The
|
|
8
|
+
* function never shells out and never reads schemas.
|
|
6
9
|
*/
|
|
7
|
-
import { existsSync
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { execFile } from "node:child_process";
|
|
12
|
-
import { promisify } from "node:util";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { readStackEnv } from "./secrets.js";
|
|
12
|
+
import { getCoreSecretMappings } from "./secret-mappings.js";
|
|
13
13
|
import type { ControlPlaneState } from "./types.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
let VARLOCK_BIN = "varlock";
|
|
20
|
-
if (envVarlockBin) {
|
|
21
|
-
if (envVarlockBin === "varlock" || envVarlockBin.startsWith("/")) {
|
|
22
|
-
VARLOCK_BIN = envVarlockBin;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function sanitizeVarlockMessage(msg: string): string {
|
|
27
|
-
return msg
|
|
28
|
-
.replace(/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]")
|
|
29
|
-
.replace(/gsk_[A-Za-z0-9]{30,}/g, "[REDACTED]")
|
|
30
|
-
.replace(/AIza[A-Za-z0-9_\-]{35}/g, "[REDACTED]")
|
|
31
|
-
.replace(/[0-9a-f]{32,}/gi, "[REDACTED]")
|
|
32
|
-
.replace(/value '([^']*)'/g, "value '[REDACTED]'");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function runVarlockLoad(
|
|
36
|
-
schemaFile: string,
|
|
37
|
-
envFile: string,
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "varlock-"));
|
|
40
|
-
try {
|
|
41
|
-
copyFileSync(schemaFile, join(tmpDir, ".env.schema"));
|
|
42
|
-
copyFileSync(envFile, join(tmpDir, ".env"));
|
|
43
|
-
await execFileAsync(
|
|
44
|
-
VARLOCK_BIN,
|
|
45
|
-
["load", "--path", `${tmpDir}/`],
|
|
46
|
-
{ timeout: 10000 },
|
|
47
|
-
);
|
|
48
|
-
} finally {
|
|
49
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
15
|
+
// Stack-scoped env keys that must always exist and carry a non-empty value
|
|
16
|
+
// for the platform to boot. Keep this list small — anything optional
|
|
17
|
+
// belongs in the warning bucket instead.
|
|
18
|
+
const REQUIRED_STACK_KEYS = ["OP_UI_LOGIN_PASSWORD"] as const;
|
|
52
19
|
|
|
53
20
|
/**
|
|
54
|
-
* Validate the
|
|
21
|
+
* Validate the live configuration files.
|
|
55
22
|
*
|
|
56
23
|
* Checks:
|
|
57
|
-
* 1.
|
|
58
|
-
*
|
|
24
|
+
* 1. config/stack/stack.env exists and carries every required key with a
|
|
25
|
+
* non-empty value.
|
|
26
|
+
* 2. Every secret env key in getCoreSecretMappings() is present (key only
|
|
27
|
+
* — blank values are warned about, never erred on, because operators
|
|
28
|
+
* may opt out of providers they don't use).
|
|
29
|
+
*
|
|
30
|
+
* Errors fail the result. Warnings do not. The function never reads
|
|
31
|
+
* schema files and never spawns subprocesses.
|
|
59
32
|
*/
|
|
60
33
|
export async function validateProposedState(state: ControlPlaneState): Promise<{
|
|
61
34
|
ok: boolean;
|
|
@@ -64,44 +37,36 @@ export async function validateProposedState(state: ControlPlaneState): Promise<{
|
|
|
64
37
|
}> {
|
|
65
38
|
const errors: string[] = [];
|
|
66
39
|
const warnings: string[] = [];
|
|
67
|
-
let anyFailed = false;
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else if (trimmed.includes("WARN")) warnings.push(trimmed);
|
|
75
|
-
}
|
|
41
|
+
const stackEnvPath = `${state.stackDir}/stack.env`;
|
|
42
|
+
|
|
43
|
+
if (!existsSync(stackEnvPath)) {
|
|
44
|
+
errors.push(`ERROR: stack env file missing at ${stackEnvPath}`);
|
|
45
|
+
return { ok: false, errors, warnings };
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
anyFailed = true;
|
|
86
|
-
if (err && typeof err === "object" && "stderr" in err) {
|
|
87
|
-
collectOutput(String((err as { stderr: string }).stderr));
|
|
88
|
-
}
|
|
48
|
+
const stackEnv = readStackEnv(state.stackDir);
|
|
49
|
+
const userEnv: Record<string, string> = {};
|
|
50
|
+
|
|
51
|
+
for (const key of REQUIRED_STACK_KEYS) {
|
|
52
|
+
const value = stackEnv[key];
|
|
53
|
+
if (!value || value.trim().length === 0) {
|
|
54
|
+
errors.push(`ERROR: required key ${key} is missing or empty in config/stack/stack.env`);
|
|
89
55
|
}
|
|
90
56
|
}
|
|
91
57
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
58
|
+
// Every canonical secret should at least appear as a key somewhere in
|
|
59
|
+
// the env files so the operator sees the slot. Missing slots warn (not
|
|
60
|
+
// error) since not every provider is in use on every install.
|
|
61
|
+
for (const mapping of getCoreSecretMappings(stackEnv)) {
|
|
62
|
+
const inStack = Object.prototype.hasOwnProperty.call(stackEnv, mapping.envKey);
|
|
63
|
+
const inUser = Object.prototype.hasOwnProperty.call(userEnv, mapping.envKey);
|
|
64
|
+
if (!inStack && !inUser) {
|
|
65
|
+
warnings.push(
|
|
66
|
+
`WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in config/stack/stack.env`,
|
|
67
|
+
);
|
|
103
68
|
}
|
|
104
69
|
}
|
|
105
70
|
|
|
106
|
-
return { ok:
|
|
71
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
107
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,21 +10,25 @@
|
|
|
10
10
|
export {
|
|
11
11
|
LLM_PROVIDERS,
|
|
12
12
|
EMBEDDING_DIMS,
|
|
13
|
+
lookupEmbeddingDims,
|
|
13
14
|
} from "./provider-constants.js";
|
|
14
15
|
|
|
15
16
|
// ── Logger ──────────────────────────────────────────────────────────────
|
|
16
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
createLogger,
|
|
19
|
+
isSensitiveEnvKey,
|
|
20
|
+
redactValue,
|
|
21
|
+
redactExtra,
|
|
22
|
+
} from "./logger.js";
|
|
17
23
|
|
|
18
24
|
// ── Types ───────────────────────────────────────────────────────────────
|
|
19
25
|
export type {
|
|
20
26
|
ControlPlaneState,
|
|
21
27
|
CoreServiceName,
|
|
22
28
|
OptionalServiceName,
|
|
23
|
-
AccessScope,
|
|
24
29
|
ChannelInfo,
|
|
25
30
|
CallerType,
|
|
26
31
|
ArtifactMeta,
|
|
27
|
-
AuditEntry,
|
|
28
32
|
} from "./control-plane/types.js";
|
|
29
33
|
export {
|
|
30
34
|
CORE_SERVICES,
|
|
@@ -62,30 +66,35 @@ export {
|
|
|
62
66
|
uninstallAutomation,
|
|
63
67
|
} from "./control-plane/registry.js";
|
|
64
68
|
|
|
65
|
-
// ── Home Layout (v0.
|
|
69
|
+
// ── Home Layout (v0.11.0) ───────────────────────────────────────────────
|
|
66
70
|
export {
|
|
67
71
|
resolveOpenPalmHome,
|
|
68
72
|
resolveConfigDir,
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
resolveStashDir,
|
|
74
|
+
resolveWorkspaceDir,
|
|
75
|
+
resolveCacheDir,
|
|
76
|
+
resolveStateDir,
|
|
77
|
+
resolveStackDir,
|
|
71
78
|
resolveLogsDir,
|
|
72
|
-
resolveCacheHome,
|
|
73
|
-
resolveRegistryDir,
|
|
74
|
-
resolveRegistryAddonsDir,
|
|
75
|
-
resolveRegistryAutomationsDir,
|
|
76
79
|
ensureHomeDirs,
|
|
77
80
|
} from "./control-plane/home.js";
|
|
78
81
|
|
|
82
|
+
// ── Path Resolution ─────────────────────────────────────────────────────
|
|
83
|
+
export * from "./control-plane/paths.js";
|
|
84
|
+
|
|
79
85
|
// ── Env ─────────────────────────────────────────────────────────────────
|
|
80
86
|
export {
|
|
81
87
|
parseEnvContent,
|
|
82
88
|
parseEnvFile,
|
|
89
|
+
expandEnvVars,
|
|
83
90
|
mergeEnvContent,
|
|
91
|
+
removeEnvKey,
|
|
92
|
+
upsertEnvValue,
|
|
93
|
+
resolveRequestedImageTag,
|
|
94
|
+
reconcileStackEnvImageTag,
|
|
95
|
+
RELEASE_TAG_REGEX,
|
|
84
96
|
} from "./control-plane/env.js";
|
|
85
97
|
|
|
86
|
-
// ── Audit ───────────────────────────────────────────────────────────────
|
|
87
|
-
export { appendAudit } from "./control-plane/audit.js";
|
|
88
|
-
|
|
89
98
|
// ── OpenCode Client ─────────────────────────────────────────────────────
|
|
90
99
|
export { createOpenCodeClient } from "./control-plane/opencode-client.js";
|
|
91
100
|
export type { ProxyResult, OpenCodeProvider } from "./control-plane/opencode-client.js";
|
|
@@ -95,15 +104,19 @@ export {
|
|
|
95
104
|
PLAIN_CONFIG_KEYS,
|
|
96
105
|
ensureSecrets,
|
|
97
106
|
updateSecretsEnv,
|
|
107
|
+
writeAuthJsonProviderKeys,
|
|
98
108
|
readStackEnv,
|
|
99
109
|
patchSecretsEnvFile,
|
|
100
110
|
maskSecretValue,
|
|
101
111
|
ensureOpenCodeConfig,
|
|
102
112
|
} from "./control-plane/secrets.js";
|
|
113
|
+
export { migrateAuth0110 } from "./control-plane/migrate-0110.js";
|
|
114
|
+
export type { MigrateAuth0110Result } from "./control-plane/migrate-0110.js";
|
|
103
115
|
export {
|
|
104
116
|
detectSecretBackend,
|
|
105
117
|
validatePassEntryName,
|
|
106
118
|
} from "./control-plane/secret-backend.js";
|
|
119
|
+
export type { SecretBackend } from "./control-plane/secret-backend.js";
|
|
107
120
|
// ── Setup Status ────────────────────────────────────────────────────────
|
|
108
121
|
export {
|
|
109
122
|
isSetupComplete,
|
|
@@ -114,35 +127,24 @@ export {
|
|
|
114
127
|
discoverChannels,
|
|
115
128
|
isAllowedService,
|
|
116
129
|
isValidChannel,
|
|
117
|
-
isChannelAddon,
|
|
118
130
|
} from "./control-plane/channels.js";
|
|
119
131
|
|
|
120
|
-
// ──
|
|
132
|
+
// ── Provider Model Discovery ────────────────────────────────────────────
|
|
121
133
|
export type {
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
ProviderModelsResult,
|
|
135
|
+
ModelDiscoveryReason,
|
|
136
|
+
} from "./control-plane/provider-models.js";
|
|
124
137
|
export {
|
|
125
|
-
EMBED_PROVIDERS,
|
|
126
|
-
resolveApiKey,
|
|
127
138
|
fetchProviderModels,
|
|
128
|
-
|
|
129
|
-
readMemoryConfig,
|
|
130
|
-
writeMemoryConfig,
|
|
131
|
-
ensureMemoryConfig,
|
|
132
|
-
checkVectorDimensions,
|
|
133
|
-
resetVectorStore,
|
|
134
|
-
provisionMemoryUser,
|
|
135
|
-
} from "./control-plane/memory-config.js";
|
|
139
|
+
} from "./control-plane/provider-models.js";
|
|
136
140
|
|
|
137
141
|
// ── Core Assets ─────────────────────────────────────────────────────────
|
|
138
142
|
export {
|
|
139
|
-
ensureUserEnvSchema,
|
|
140
|
-
ensureSystemEnvSchema,
|
|
141
|
-
ensureMemoryDir,
|
|
142
143
|
ensureCoreCompose,
|
|
143
144
|
readCoreCompose,
|
|
144
145
|
ensureOpenCodeSystemConfig,
|
|
145
146
|
refreshCoreAssets,
|
|
147
|
+
seedStashAssets,
|
|
146
148
|
} from "./control-plane/core-assets.js";
|
|
147
149
|
|
|
148
150
|
// ── Configuration Persistence ────────────────────────────────────────────
|
|
@@ -157,6 +159,7 @@ export {
|
|
|
157
159
|
writeSystemEnv,
|
|
158
160
|
readChannelSecrets,
|
|
159
161
|
writeChannelSecrets,
|
|
162
|
+
ensureComposeVolumeTargets,
|
|
160
163
|
} from "./control-plane/config-persistence.js";
|
|
161
164
|
|
|
162
165
|
// ── Rollback ─────────────────────────────────────────────────────────────
|
|
@@ -184,7 +187,6 @@ export {
|
|
|
184
187
|
buildManagedServices,
|
|
185
188
|
normalizeCaller,
|
|
186
189
|
} from "./control-plane/lifecycle.js";
|
|
187
|
-
export type { UpgradeResult } from "./control-plane/lifecycle.js";
|
|
188
190
|
|
|
189
191
|
// ── Docker ──────────────────────────────────────────────────────────────
|
|
190
192
|
export type { DockerResult } from "./control-plane/docker.js";
|
|
@@ -204,22 +206,20 @@ export {
|
|
|
204
206
|
composePull,
|
|
205
207
|
composeStats,
|
|
206
208
|
getDockerEvents,
|
|
207
|
-
|
|
209
|
+
inspectContainerStatus,
|
|
208
210
|
} from "./control-plane/docker.js";
|
|
209
211
|
|
|
210
212
|
// ── Scheduler ───────────────────────────────────────────────────────────
|
|
211
213
|
export type {
|
|
212
|
-
ActionType,
|
|
213
|
-
AutomationAction,
|
|
214
214
|
AutomationConfig,
|
|
215
|
-
|
|
215
|
+
AutomationRunResult,
|
|
216
216
|
} from "./control-plane/scheduler.js";
|
|
217
217
|
export {
|
|
218
218
|
SCHEDULE_PRESETS,
|
|
219
|
-
resolveSchedule,
|
|
220
|
-
parseAutomationYaml,
|
|
221
219
|
loadAutomations,
|
|
222
|
-
|
|
220
|
+
executeAutomation,
|
|
221
|
+
syncAutomations,
|
|
222
|
+
readAutomationLogs,
|
|
223
223
|
} from "./control-plane/scheduler.js";
|
|
224
224
|
|
|
225
225
|
// ── Model Runner (local provider detection) ─────────────────────────────
|
|
@@ -232,28 +232,27 @@ export {
|
|
|
232
232
|
buildComposeCliArgs,
|
|
233
233
|
} from "./control-plane/compose-args.js";
|
|
234
234
|
|
|
235
|
+
// ── Compose Error Parsing ────────────────────────────────────────────────
|
|
236
|
+
export type { ComposeServiceFailure } from "./control-plane/compose-errors.js";
|
|
237
|
+
export {
|
|
238
|
+
parseComposeStderr,
|
|
239
|
+
summarizeComposeStderr,
|
|
240
|
+
} from "./control-plane/compose-errors.js";
|
|
241
|
+
|
|
235
242
|
// ── Stack Spec (v2) ──────────────────────────────────────────────────────
|
|
236
243
|
export type {
|
|
237
244
|
StackSpec,
|
|
238
|
-
StackSpecCapabilities,
|
|
239
|
-
StackSpecEmbeddings,
|
|
240
|
-
StackSpecMemory,
|
|
241
|
-
StackSpecTts,
|
|
242
|
-
StackSpecStt,
|
|
243
|
-
StackSpecReranker,
|
|
244
245
|
} from "./control-plane/stack-spec.js";
|
|
245
246
|
export {
|
|
246
247
|
STACK_SPEC_FILENAME,
|
|
247
248
|
writeStackSpec,
|
|
248
249
|
readStackSpec,
|
|
249
|
-
updateCapability,
|
|
250
|
-
parseCapabilityString,
|
|
251
|
-
formatCapabilityString,
|
|
252
250
|
} from "./control-plane/stack-spec.js";
|
|
253
251
|
|
|
254
252
|
// ── Spec-to-Env Derivation ──────────────────────────────────────────────
|
|
253
|
+
export type { VoiceVarsConfig } from "./control-plane/spec-to-env.js";
|
|
255
254
|
export {
|
|
256
|
-
|
|
255
|
+
writeVoiceVars,
|
|
257
256
|
} from "./control-plane/spec-to-env.js";
|
|
258
257
|
|
|
259
258
|
// ── Setup ────────────────────────────────────────────────────────────────
|
|
@@ -265,3 +264,42 @@ export type {
|
|
|
265
264
|
export {
|
|
266
265
|
performSetup,
|
|
267
266
|
} from "./control-plane/setup.js";
|
|
267
|
+
|
|
268
|
+
// ── Install Lock (shared between performSetup and startDeploy) ───────────
|
|
269
|
+
export type { InstallLockHandle } from "./control-plane/install-lock.js";
|
|
270
|
+
export {
|
|
271
|
+
acquireInstallLock,
|
|
272
|
+
releaseInstallLock,
|
|
273
|
+
} from "./control-plane/install-lock.js";
|
|
274
|
+
|
|
275
|
+
// ── Host OpenCode Import ─────────────────────────────────────────────────
|
|
276
|
+
export type {
|
|
277
|
+
HostOpenCodeStatus,
|
|
278
|
+
HostImportResult,
|
|
279
|
+
} from "./control-plane/host-opencode.js";
|
|
280
|
+
export {
|
|
281
|
+
detectHostOpenCode,
|
|
282
|
+
importHostOpenCode,
|
|
283
|
+
} from "./control-plane/host-opencode.js";
|
|
284
|
+
|
|
285
|
+
// ── AKM Vault ────────────────────────────────────────────────────────────
|
|
286
|
+
export {
|
|
287
|
+
AKM_USER_VAULT_REF,
|
|
288
|
+
buildAkmEnv,
|
|
289
|
+
ensureAkmUserVault,
|
|
290
|
+
writeAkmVaultKey,
|
|
291
|
+
deleteAkmVaultKey,
|
|
292
|
+
readAkmUserVaultFile,
|
|
293
|
+
readUserVaultSync,
|
|
294
|
+
} from "./control-plane/akm-vault.js";
|
|
295
|
+
|
|
296
|
+
// ── UI asset seeding and resolution ─────────────────────────────────────────
|
|
297
|
+
export type { UiBuildUpdateResult } from "./control-plane/ui-assets.js";
|
|
298
|
+
export {
|
|
299
|
+
resolveLocalOpenpalmDir,
|
|
300
|
+
seedOpenPalmDir,
|
|
301
|
+
resolveLocalUiBuild,
|
|
302
|
+
resolveUiBuildDir,
|
|
303
|
+
seedUiBuild,
|
|
304
|
+
checkAndUpdateUiBuild,
|
|
305
|
+
} from "./control-plane/ui-assets.js";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the in-house log redactor introduced in #391 (replacing varlock).
|
|
3
|
+
*
|
|
4
|
+
* The contract:
|
|
5
|
+
* - keys matching the word-bounded pattern
|
|
6
|
+
* `(^|_)(TOKEN|SECRET|KEY|PASSWORD|HMAC)(_|$)` (case-insensitive)
|
|
7
|
+
* have their value replaced with `'***REDACTED***'`.
|
|
8
|
+
* - substring false positives (e.g. `MONKEY`, `PACKET_SIZE`) are NOT redacted.
|
|
9
|
+
* - non-string sensitive values (numbers, booleans) are still redacted.
|
|
10
|
+
* - non-secret keys are passed through unchanged.
|
|
11
|
+
* - nested objects and arrays are walked recursively.
|
|
12
|
+
* - createLogger() applies the same masking before writing to stdout/stderr
|
|
13
|
+
* at every log level (debug/info/warn/error).
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
16
|
+
import {
|
|
17
|
+
createLogger,
|
|
18
|
+
isSensitiveEnvKey,
|
|
19
|
+
redactValue,
|
|
20
|
+
redactExtra,
|
|
21
|
+
} from './logger.js';
|
|
22
|
+
|
|
23
|
+
describe('redactValue', () => {
|
|
24
|
+
test('masks values for sensitive key suffixes', () => {
|
|
25
|
+
expect(redactValue('OPENAI_API_KEY', 'sk-abc')).toBe('***REDACTED***');
|
|
26
|
+
expect(redactValue('SLACK_BOT_TOKEN', 'xoxb-123')).toBe('***REDACTED***');
|
|
27
|
+
expect(redactValue('CHANNEL_DISCORD_SECRET', 'hmac-bytes')).toBe('***REDACTED***');
|
|
28
|
+
expect(redactValue('OP_OPENCODE_PASSWORD', 'hunter2')).toBe('***REDACTED***');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('matches case-insensitively', () => {
|
|
32
|
+
expect(redactValue('openai_api_key', 'sk-xyz')).toBe('***REDACTED***');
|
|
33
|
+
expect(redactValue('My_Token', 'abc')).toBe('***REDACTED***');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('leaves non-secret values alone', () => {
|
|
37
|
+
expect(redactValue('OWNER_NAME', 'alice')).toBe('alice');
|
|
38
|
+
expect(redactValue('OP_HOME', '/openpalm')).toBe('/openpalm');
|
|
39
|
+
expect(redactValue('OP_ASSISTANT_PORT', '3800')).toBe('3800');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('isSensitiveEnvKey', () => {
|
|
44
|
+
test('returns true for token/secret/key/password/hmac suffix keys', () => {
|
|
45
|
+
expect(isSensitiveEnvKey('FOO_TOKEN')).toBe(true);
|
|
46
|
+
expect(isSensitiveEnvKey('FOO_SECRET')).toBe(true);
|
|
47
|
+
expect(isSensitiveEnvKey('FOO_KEY')).toBe(true);
|
|
48
|
+
expect(isSensitiveEnvKey('FOO_PASSWORD')).toBe(true);
|
|
49
|
+
expect(isSensitiveEnvKey('CHANNEL_FOO_HMAC')).toBe(true);
|
|
50
|
+
expect(isSensitiveEnvKey('OP_UI_TOKEN')).toBe(true);
|
|
51
|
+
expect(isSensitiveEnvKey('CHANNEL_API_KEY')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('returns true for bare or prefix forms', () => {
|
|
55
|
+
expect(isSensitiveEnvKey('TOKEN')).toBe(true);
|
|
56
|
+
expect(isSensitiveEnvKey('SECRET')).toBe(true);
|
|
57
|
+
expect(isSensitiveEnvKey('KEY')).toBe(true);
|
|
58
|
+
expect(isSensitiveEnvKey('HMAC_KEY')).toBe(true);
|
|
59
|
+
expect(isSensitiveEnvKey('PASSWORD_HASH')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('returns false for substring false positives', () => {
|
|
63
|
+
// MONKEY contains the substring KEY but no underscore boundary.
|
|
64
|
+
expect(isSensitiveEnvKey('MONKEY')).toBe(false);
|
|
65
|
+
// PACKET_SIZE: KET is not one of the words; ET_SIZE is unrelated.
|
|
66
|
+
expect(isSensitiveEnvKey('PACKET_SIZE')).toBe(false);
|
|
67
|
+
// MARKETING_KEYWORD: KEYWORD does not have a trailing underscore or EOL.
|
|
68
|
+
expect(isSensitiveEnvKey('MARKETING_KEYWORD')).toBe(false);
|
|
69
|
+
// KEYBOARD: starts with KEY but not followed by underscore or EOL.
|
|
70
|
+
expect(isSensitiveEnvKey('KEYBOARD')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('returns false for ordinary keys', () => {
|
|
74
|
+
expect(isSensitiveEnvKey('OWNER_NAME')).toBe(false);
|
|
75
|
+
expect(isSensitiveEnvKey('OP_HOME')).toBe(false);
|
|
76
|
+
expect(isSensitiveEnvKey('OP_ASSISTANT_PORT')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('redactExtra', () => {
|
|
81
|
+
test('masks top-level secret string values', () => {
|
|
82
|
+
const result = redactExtra({
|
|
83
|
+
OPENAI_API_KEY: 'sk-abc',
|
|
84
|
+
OWNER_NAME: 'alice',
|
|
85
|
+
});
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
88
|
+
OWNER_NAME: 'alice',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('walks nested objects', () => {
|
|
93
|
+
const result = redactExtra({
|
|
94
|
+
env: {
|
|
95
|
+
OPENAI_API_KEY: 'sk-abc',
|
|
96
|
+
OWNER_NAME: 'alice',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
env: {
|
|
101
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
102
|
+
OWNER_NAME: 'alice',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('handles arrays of objects', () => {
|
|
108
|
+
const result = redactExtra({
|
|
109
|
+
items: [
|
|
110
|
+
{ OPENAI_API_KEY: 'sk-1' },
|
|
111
|
+
{ OWNER_NAME: 'bob' },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
expect(result).toEqual({
|
|
115
|
+
items: [
|
|
116
|
+
{ OPENAI_API_KEY: '***REDACTED***' },
|
|
117
|
+
{ OWNER_NAME: 'bob' },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('returns primitive inputs unchanged', () => {
|
|
123
|
+
expect(redactExtra('plain')).toBe('plain');
|
|
124
|
+
expect(redactExtra(42)).toBe(42);
|
|
125
|
+
expect(redactExtra(null)).toBe(null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('redacts non-string sensitive values (numbers, booleans)', () => {
|
|
129
|
+
const result = redactExtra({
|
|
130
|
+
OP_UI_TOKEN: 12345,
|
|
131
|
+
OPENAI_API_KEY: true,
|
|
132
|
+
OWNER_NAME: 'alice',
|
|
133
|
+
});
|
|
134
|
+
expect(result).toEqual({
|
|
135
|
+
OP_UI_TOKEN: '***REDACTED***',
|
|
136
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
137
|
+
OWNER_NAME: 'alice',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('does not redact substring false positives', () => {
|
|
142
|
+
const result = redactExtra({
|
|
143
|
+
MONKEY: 'banana',
|
|
144
|
+
PACKET_SIZE: 1500,
|
|
145
|
+
MARKETING_KEYWORD: 'free',
|
|
146
|
+
OPENAI_API_KEY: 'sk-leak',
|
|
147
|
+
});
|
|
148
|
+
expect(result).toEqual({
|
|
149
|
+
MONKEY: 'banana',
|
|
150
|
+
PACKET_SIZE: 1500,
|
|
151
|
+
MARKETING_KEYWORD: 'free',
|
|
152
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('redacts CHANNEL_FOO_HMAC values', () => {
|
|
157
|
+
const result = redactExtra({ CHANNEL_DISCORD_HMAC: 'hmac-bytes' });
|
|
158
|
+
expect(result).toEqual({ CHANNEL_DISCORD_HMAC: '***REDACTED***' });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('createLogger', () => {
|
|
163
|
+
const origLog = console.log;
|
|
164
|
+
const origErr = console.error;
|
|
165
|
+
let logged: string[] = [];
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
logged = [];
|
|
169
|
+
console.log = (...args: unknown[]) => {
|
|
170
|
+
logged.push(args.map((a) => String(a)).join(' '));
|
|
171
|
+
};
|
|
172
|
+
console.error = (...args: unknown[]) => {
|
|
173
|
+
logged.push(args.map((a) => String(a)).join(' '));
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterEach(() => {
|
|
178
|
+
console.log = origLog;
|
|
179
|
+
console.error = origErr;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('redacts sensitive keys in the extra payload before writing the log line', () => {
|
|
183
|
+
const logger = createLogger('test');
|
|
184
|
+
logger.info('msg', { OPENAI_API_KEY: 'sk-leak', OWNER_NAME: 'alice' });
|
|
185
|
+
expect(logged.length).toBe(1);
|
|
186
|
+
expect(logged[0]).toContain('"OPENAI_API_KEY":"***REDACTED***"');
|
|
187
|
+
expect(logged[0]).toContain('"OWNER_NAME":"alice"');
|
|
188
|
+
expect(logged[0]).not.toContain('sk-leak');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('error level still goes through redaction', () => {
|
|
192
|
+
const logger = createLogger('test');
|
|
193
|
+
logger.error('boom', { OP_UI_TOKEN: 'tok-leak' });
|
|
194
|
+
expect(logged[0]).toContain('"OP_UI_TOKEN":"***REDACTED***"');
|
|
195
|
+
expect(logged[0]).not.toContain('tok-leak');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('warn level applies redaction', () => {
|
|
199
|
+
const logger = createLogger('test');
|
|
200
|
+
logger.warn('caution', { CHANNEL_API_KEY: 'warn-leak' });
|
|
201
|
+
expect(logged.length).toBe(1);
|
|
202
|
+
expect(logged[0]).toContain('"CHANNEL_API_KEY":"***REDACTED***"');
|
|
203
|
+
expect(logged[0]).not.toContain('warn-leak');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('debug level applies redaction', () => {
|
|
207
|
+
const logger = createLogger('test');
|
|
208
|
+
logger.debug('detail', { CHANNEL_FOO_HMAC: 'debug-leak' });
|
|
209
|
+
expect(logged.length).toBe(1);
|
|
210
|
+
expect(logged[0]).toContain('"CHANNEL_FOO_HMAC":"***REDACTED***"');
|
|
211
|
+
expect(logged[0]).not.toContain('debug-leak');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('substring false positives are not redacted at log time', () => {
|
|
215
|
+
const logger = createLogger('test');
|
|
216
|
+
logger.info('msg', { MONKEY: 'banana', PACKET_SIZE: 1500 });
|
|
217
|
+
expect(logged[0]).toContain('"MONKEY":"banana"');
|
|
218
|
+
expect(logged[0]).toContain('"PACKET_SIZE":1500');
|
|
219
|
+
expect(logged[0]).not.toContain('***REDACTED***');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('non-string sensitive values are redacted at log time', () => {
|
|
223
|
+
const logger = createLogger('test');
|
|
224
|
+
logger.info('msg', { OP_UI_TOKEN: 12345 });
|
|
225
|
+
expect(logged[0]).toContain('"OP_UI_TOKEN":"***REDACTED***"');
|
|
226
|
+
expect(logged[0]).not.toContain('12345');
|
|
227
|
+
});
|
|
228
|
+
});
|