@openpalm/lib 0.11.0-beta.9 → 0.11.0-rc.18
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 +1 -1
- package/package.json +1 -1
- package/src/control-plane/akm-sources.test.ts +206 -0
- package/src/control-plane/akm-sources.ts +234 -0
- package/src/control-plane/akm-user-env.test.ts +142 -0
- package/src/control-plane/akm-user-env.ts +167 -0
- package/src/control-plane/backup.ts +14 -5
- package/src/control-plane/channels.ts +48 -29
- package/src/control-plane/cleanup-guardrails.test.ts +1 -1
- package/src/control-plane/compose-args.test.ts +69 -30
- package/src/control-plane/compose-args.ts +62 -8
- package/src/control-plane/config-persistence.ts +102 -136
- package/src/control-plane/core-assets.ts +45 -60
- package/src/control-plane/defaults.ts +16 -0
- package/src/control-plane/docker.ts +15 -14
- package/src/control-plane/env.test.ts +10 -10
- package/src/control-plane/env.ts +16 -1
- package/src/control-plane/extends-support.test.ts +8 -8
- package/src/control-plane/fs-atomic.ts +15 -0
- package/src/control-plane/home.ts +34 -46
- package/src/control-plane/host-akm-sharing.test.ts +145 -0
- package/src/control-plane/host-akm-sharing.ts +129 -0
- package/src/control-plane/host-opencode.test.ts +82 -10
- package/src/control-plane/host-opencode.ts +42 -13
- package/src/control-plane/install-edge-cases.test.ts +100 -136
- package/src/control-plane/install-lock.ts +7 -7
- package/src/control-plane/lifecycle.ts +45 -40
- package/src/control-plane/markdown-task.ts +30 -50
- package/src/control-plane/migrations.test.ts +272 -0
- package/src/control-plane/migrations.ts +423 -0
- package/src/control-plane/opencode-client.ts +1 -1
- package/src/control-plane/paths.ts +61 -46
- package/src/control-plane/profile-ids.ts +21 -0
- package/src/control-plane/provider-models.ts +3 -3
- package/src/control-plane/registry.test.ts +107 -90
- package/src/control-plane/registry.ts +301 -110
- package/src/control-plane/rollback.ts +8 -38
- package/src/control-plane/scheduler.ts +10 -7
- package/src/control-plane/secret-audit.test.ts +159 -0
- package/src/control-plane/secret-audit.ts +255 -0
- package/src/control-plane/secret-mappings.ts +2 -2
- package/src/control-plane/secrets-files.test.ts +99 -0
- package/src/control-plane/secrets-files.ts +113 -0
- package/src/control-plane/secrets.ts +113 -86
- package/src/control-plane/setup-config.schema.json +1 -1
- package/src/control-plane/setup-status.ts +6 -11
- package/src/control-plane/setup.test.ts +137 -61
- package/src/control-plane/setup.ts +82 -63
- package/src/control-plane/skeleton-guardrail.test.ts +66 -56
- package/src/control-plane/spec-to-env.test.ts +63 -26
- package/src/control-plane/spec-to-env.ts +51 -14
- package/src/control-plane/task-files.test.ts +45 -0
- package/src/control-plane/task-files.ts +51 -0
- package/src/control-plane/types.ts +2 -4
- package/src/control-plane/ui-assets.test.ts +333 -0
- package/src/control-plane/ui-assets.ts +290 -142
- package/src/control-plane/validate.ts +13 -15
- package/src/index.ts +96 -26
- package/src/control-plane/akm-vault.test.ts +0 -105
- package/src/control-plane/akm-vault.ts +0 -311
- package/src/control-plane/core-assets.test.ts +0 -104
- package/src/control-plane/migrate-0110.test.ts +0 -177
- package/src/control-plane/migrate-0110.ts +0 -99
- package/src/control-plane/registry-components.test.ts +0 -391
- package/src/control-plane/stack-spec.test.ts +0 -94
- package/src/control-plane/stack-spec.ts +0 -67
|
@@ -16,10 +16,9 @@ function makeState(homeDir: string): ControlPlaneState {
|
|
|
16
16
|
return {
|
|
17
17
|
homeDir,
|
|
18
18
|
configDir: join(homeDir, "config"),
|
|
19
|
-
stashDir: join(homeDir, "
|
|
19
|
+
stashDir: join(homeDir, "knowledge"),
|
|
20
20
|
workspaceDir: join(homeDir, "workspace"),
|
|
21
|
-
|
|
22
|
-
stateDir: join(homeDir, "state"),
|
|
21
|
+
dataDir: join(homeDir, "data"),
|
|
23
22
|
stackDir: join(homeDir, "config/stack"),
|
|
24
23
|
services: {},
|
|
25
24
|
artifacts: { compose: "" },
|
|
@@ -104,6 +103,48 @@ describe("detectHostOpenCode", () => {
|
|
|
104
103
|
expect(status.providerCount).toBe(0);
|
|
105
104
|
});
|
|
106
105
|
});
|
|
106
|
+
|
|
107
|
+
it("returns modelPreferences when model and small_model are set", () => {
|
|
108
|
+
const configDir = join(xdgRoot, "config", "opencode");
|
|
109
|
+
mkdirSync(configDir, { recursive: true });
|
|
110
|
+
writeFileSync(join(configDir, "opencode.json"), JSON.stringify({
|
|
111
|
+
provider: { groq: {} },
|
|
112
|
+
model: "groq/llama-3.3-70b-versatile",
|
|
113
|
+
small_model: "groq/llama-3.1-8b-instant",
|
|
114
|
+
}));
|
|
115
|
+
withXdgEnv(`${xdgRoot}/config`, `${xdgRoot}/data`, () => {
|
|
116
|
+
const status = detectHostOpenCode();
|
|
117
|
+
expect(status.modelPreferences).toBeDefined();
|
|
118
|
+
expect(status.modelPreferences?.model).toBe("groq/llama-3.3-70b-versatile");
|
|
119
|
+
expect(status.modelPreferences?.small_model).toBe("groq/llama-3.1-8b-instant");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("omits modelPreferences when no model fields are set", () => {
|
|
124
|
+
const configDir = join(xdgRoot, "config", "opencode");
|
|
125
|
+
mkdirSync(configDir, { recursive: true });
|
|
126
|
+
writeFileSync(join(configDir, "opencode.json"), JSON.stringify({
|
|
127
|
+
provider: { groq: {} },
|
|
128
|
+
}));
|
|
129
|
+
withXdgEnv(`${xdgRoot}/config`, `${xdgRoot}/data`, () => {
|
|
130
|
+
const status = detectHostOpenCode();
|
|
131
|
+
expect(status.modelPreferences).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns partial modelPreferences when only model is set", () => {
|
|
136
|
+
const configDir = join(xdgRoot, "config", "opencode");
|
|
137
|
+
mkdirSync(configDir, { recursive: true });
|
|
138
|
+
writeFileSync(join(configDir, "opencode.json"), JSON.stringify({
|
|
139
|
+
provider: { anthropic: {} },
|
|
140
|
+
model: "anthropic/claude-sonnet-4-5",
|
|
141
|
+
}));
|
|
142
|
+
withXdgEnv(`${xdgRoot}/config`, `${xdgRoot}/data`, () => {
|
|
143
|
+
const status = detectHostOpenCode();
|
|
144
|
+
expect(status.modelPreferences?.model).toBe("anthropic/claude-sonnet-4-5");
|
|
145
|
+
expect(status.modelPreferences?.small_model).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
107
148
|
});
|
|
108
149
|
|
|
109
150
|
// ── importHostOpenCode ────────────────────────────────────────────────────────
|
|
@@ -132,6 +173,8 @@ describe("importHostOpenCode", () => {
|
|
|
132
173
|
writeFileSync(join(hostConfigDir, "opencode.json"), JSON.stringify({
|
|
133
174
|
provider: { anthropic: { name: "Anthropic" }, groq: {} },
|
|
134
175
|
model: "anthropic/claude-3-5-sonnet",
|
|
176
|
+
small_model: "openai/gpt-4o-mini",
|
|
177
|
+
disabled_providers: ["groq"],
|
|
135
178
|
// These should be stripped:
|
|
136
179
|
plugin: [{ module: "some-plugin" }],
|
|
137
180
|
mcp: { server: {} },
|
|
@@ -153,14 +196,16 @@ describe("importHostOpenCode", () => {
|
|
|
153
196
|
const destConfig = JSON.parse(readFileSync(join(opHome, "config", "assistant", "opencode.json"), "utf-8"));
|
|
154
197
|
expect(destConfig.provider).toEqual({ anthropic: { name: "Anthropic" }, groq: {} });
|
|
155
198
|
expect(destConfig.model).toBe("anthropic/claude-3-5-sonnet");
|
|
199
|
+
expect(destConfig.small_model).toBe("openai/gpt-4o-mini");
|
|
200
|
+
expect(destConfig.disabled_providers).toEqual(["groq"]);
|
|
156
201
|
expect(destConfig.plugin).toBeUndefined();
|
|
157
202
|
expect(destConfig.mcp).toBeUndefined();
|
|
158
203
|
|
|
159
204
|
// Verify auth.json was written
|
|
160
|
-
expect(existsSync(join(opHome, "
|
|
205
|
+
expect(existsSync(join(opHome, "knowledge", "secrets", "auth.json"))).toBe(true);
|
|
161
206
|
|
|
162
207
|
// Verify auth.json permissions are 0o600
|
|
163
|
-
const authStat = statSync(join(opHome, "
|
|
208
|
+
const authStat = statSync(join(opHome, "knowledge", "secrets", "auth.json"));
|
|
164
209
|
// On Linux, mode & 0o777 extracts permission bits
|
|
165
210
|
expect(authStat.mode & 0o777).toBe(0o600);
|
|
166
211
|
});
|
|
@@ -217,6 +262,34 @@ describe("importHostOpenCode", () => {
|
|
|
217
262
|
expect(written.provider.anthropic.name).toBe("Host Anthropic");
|
|
218
263
|
});
|
|
219
264
|
|
|
265
|
+
it("keeps existing model defaults and fills only missing host fields", () => {
|
|
266
|
+
const hostConfigDir = join(xdgRoot, "config", "opencode");
|
|
267
|
+
mkdirSync(hostConfigDir, { recursive: true });
|
|
268
|
+
writeFileSync(join(hostConfigDir, "opencode.json"), JSON.stringify({
|
|
269
|
+
provider: { openai: { name: "Host OpenAI" } },
|
|
270
|
+
model: "openai/gpt-4.1",
|
|
271
|
+
small_model: "openai/gpt-4.1-mini",
|
|
272
|
+
disabled_providers: ["groq"],
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
const state = makeState(opHome);
|
|
276
|
+
const destDir = join(opHome, "config", "assistant");
|
|
277
|
+
mkdirSync(destDir, { recursive: true });
|
|
278
|
+
writeFileSync(join(destDir, "opencode.json"), JSON.stringify({
|
|
279
|
+
provider: { anthropic: { name: "Existing Anthropic" } },
|
|
280
|
+
model: "anthropic/claude-sonnet-4",
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
withXdgEnv(`${xdgRoot}/config`, `${xdgRoot}/data`, () => {
|
|
284
|
+
importHostOpenCode(state);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const written = JSON.parse(readFileSync(join(destDir, "opencode.json"), "utf-8"));
|
|
288
|
+
expect(written.model).toBe("anthropic/claude-sonnet-4");
|
|
289
|
+
expect(written.small_model).toBe("openai/gpt-4.1-mini");
|
|
290
|
+
expect(written.disabled_providers).toEqual(["groq"]);
|
|
291
|
+
});
|
|
292
|
+
|
|
220
293
|
it("returns zero counts when no host config is present", () => {
|
|
221
294
|
const state = makeState(opHome);
|
|
222
295
|
withXdgEnv(`${xdgRoot}/config`, `${xdgRoot}/data`, () => {
|
|
@@ -228,10 +301,9 @@ describe("importHostOpenCode", () => {
|
|
|
228
301
|
});
|
|
229
302
|
|
|
230
303
|
it("partial-merge auth: does not overwrite existing credential, adds new one", () => {
|
|
231
|
-
// Pre-seed OP_HOME/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
writeFileSync(join(opConfigDir, "auth.json"), JSON.stringify({
|
|
304
|
+
// Pre-seed OP_HOME/knowledge/secrets/auth.json with one existing credential
|
|
305
|
+
mkdirSync(join(opHome, "knowledge", "secrets"), { recursive: true });
|
|
306
|
+
writeFileSync(join(opHome, "knowledge", "secrets", "auth.json"), JSON.stringify({
|
|
235
307
|
azure: { type: "api", key: "existing" },
|
|
236
308
|
}));
|
|
237
309
|
|
|
@@ -252,7 +324,7 @@ describe("importHostOpenCode", () => {
|
|
|
252
324
|
});
|
|
253
325
|
|
|
254
326
|
// Verify azure key was NOT overwritten
|
|
255
|
-
const written = JSON.parse(readFileSync(join(
|
|
327
|
+
const written = JSON.parse(readFileSync(join(opHome, "knowledge", "secrets", "auth.json"), "utf-8")) as Record<string, { key: string }>;
|
|
256
328
|
expect(written.azure.key).toBe("existing");
|
|
257
329
|
// Verify groq was added
|
|
258
330
|
expect(written.groq.key).toBe("gsk-host");
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* - auth.json is copied byte-for-byte and chmodded 0o600. Its contents
|
|
12
12
|
* are never parsed, logged, or returned to callers.
|
|
13
13
|
* - opencode.json is parsed to strip plugin/mcp/permission keys before
|
|
14
|
-
* writing;
|
|
14
|
+
* writing; provider definitions are always kept, and top-level model
|
|
15
|
+
* defaults are imported only when OP_HOME does not already define them.
|
|
15
16
|
* - Conflict detection compares provider IDs; existing credentials are
|
|
16
17
|
* preserved unless overwriteConflicts=true.
|
|
17
18
|
*/
|
|
18
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, copyFileSync } from "node:fs";
|
|
19
20
|
import { homedir } from "node:os";
|
|
21
|
+
import { dirname } from "node:path";
|
|
20
22
|
import type { ControlPlaneState } from "./types.js";
|
|
21
23
|
import { authJsonPath, assistantConfigDir } from "./paths.js";
|
|
22
24
|
|
|
@@ -31,6 +33,8 @@ export type HostOpenCodeStatus = {
|
|
|
31
33
|
providerCount: number;
|
|
32
34
|
/** Number of credential entries in auth.json (0 when not found) */
|
|
33
35
|
credentialCount: number;
|
|
36
|
+
/** Model preferences from the host's opencode.json, if present */
|
|
37
|
+
modelPreferences?: { model?: string; small_model?: string };
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
export type HostImportResult = {
|
|
@@ -64,7 +68,7 @@ function hostAuthJsonPath(): string {
|
|
|
64
68
|
|
|
65
69
|
// ── opencode.json parsing ────────────────────────────────────────────────────
|
|
66
70
|
|
|
67
|
-
/** Keys that are safe to import from host opencode.json into OP_HOME config */
|
|
71
|
+
/** Keys that are safe to import from host opencode.json into OP_HOME config. */
|
|
68
72
|
const ALLOWED_CONFIG_KEYS = new Set(["$schema", "provider", "model", "small_model", "disabled_providers"]);
|
|
69
73
|
|
|
70
74
|
type OpenCodeJson = Record<string, unknown>;
|
|
@@ -78,8 +82,18 @@ function readJsonFileSafe(path: string): OpenCodeJson | null {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function stripDisallowedKeys(obj: OpenCodeJson): OpenCodeJson {
|
|
85
|
+
const next: OpenCodeJson = {};
|
|
86
|
+
if (typeof obj.$schema === 'string') next.$schema = obj.$schema;
|
|
87
|
+
if (obj.provider && typeof obj.provider === 'object' && !Array.isArray(obj.provider)) {
|
|
88
|
+
next.provider = obj.provider;
|
|
89
|
+
}
|
|
90
|
+
if (typeof obj.model === 'string') next.model = obj.model;
|
|
91
|
+
if (typeof obj.small_model === 'string') next.small_model = obj.small_model;
|
|
92
|
+
if (Array.isArray(obj.disabled_providers) && obj.disabled_providers.every((entry) => typeof entry === 'string')) {
|
|
93
|
+
next.disabled_providers = obj.disabled_providers;
|
|
94
|
+
}
|
|
81
95
|
return Object.fromEntries(
|
|
82
|
-
Object.entries(
|
|
96
|
+
Object.entries(next).filter(([k]) => ALLOWED_CONFIG_KEYS.has(k))
|
|
83
97
|
);
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -116,9 +130,16 @@ export function detectHostOpenCode(): HostOpenCodeStatus {
|
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
let providerCount = 0;
|
|
133
|
+
let modelPreferences: { model?: string; small_model?: string } | undefined;
|
|
119
134
|
if (configExists) {
|
|
120
135
|
const parsed = readJsonFileSafe(configPath);
|
|
121
136
|
providerCount = parsed ? countProviders(parsed) : 0;
|
|
137
|
+
if (parsed) {
|
|
138
|
+
const prefs: { model?: string; small_model?: string } = {};
|
|
139
|
+
if (typeof parsed.model === 'string' && parsed.model) prefs.model = parsed.model;
|
|
140
|
+
if (typeof parsed.small_model === 'string' && parsed.small_model) prefs.small_model = parsed.small_model;
|
|
141
|
+
if (prefs.model || prefs.small_model) modelPreferences = prefs;
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
let credentialCount = 0;
|
|
@@ -131,6 +152,7 @@ export function detectHostOpenCode(): HostOpenCodeStatus {
|
|
|
131
152
|
authPath: authExists ? authPath : undefined,
|
|
132
153
|
providerCount,
|
|
133
154
|
credentialCount,
|
|
155
|
+
...(modelPreferences ? { modelPreferences } : {}),
|
|
134
156
|
};
|
|
135
157
|
}
|
|
136
158
|
|
|
@@ -181,21 +203,28 @@ export function importHostOpenCode(
|
|
|
181
203
|
}
|
|
182
204
|
}
|
|
183
205
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
206
|
+
const merged: OpenCodeJson = {
|
|
207
|
+
...existing,
|
|
208
|
+
...(typeof existing.$schema === 'undefined' && typeof sanitized.$schema !== 'undefined'
|
|
209
|
+
? { $schema: sanitized.$schema }
|
|
210
|
+
: {}),
|
|
211
|
+
...(Object.keys(mergedProviders).length > 0 ? { provider: mergedProviders } : {}),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
for (const key of ["model", "small_model", "disabled_providers"] as const) {
|
|
215
|
+
if (typeof merged[key] === 'undefined' && typeof sanitized[key] !== 'undefined') {
|
|
216
|
+
merged[key] = sanitized[key];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
writeFileSync(destPath, JSON.stringify(merged, null, 2) + "\n");
|
|
221
|
+
}
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
// ── auth.json ──────────────────────────────────────────────────────────
|
|
195
225
|
if (status.authPath) {
|
|
196
226
|
const destPath = authJsonPath(state);
|
|
197
|
-
|
|
198
|
-
mkdirSync(destDir, { recursive: true });
|
|
227
|
+
mkdirSync(dirname(destPath), { recursive: true, mode: 0o700 });
|
|
199
228
|
|
|
200
229
|
if (existsSync(destPath) && !overwriteConflicts) {
|
|
201
230
|
// Merge: copy only keys that do not already exist in OP_HOME auth.json
|