@stigmer/cli 3.12.0 → 3.12.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/commands/push.d.ts.map +1 -1
- package/commands/push.js +57 -0
- package/commands/push.js.map +1 -1
- package/commands/setup.d.ts.map +1 -1
- package/commands/setup.js +7 -3
- package/commands/setup.js.map +1 -1
- package/local/daemon/launch.js +2 -6
- package/local/daemon/launch.js.map +1 -1
- package/local/llm-config.d.ts +0 -6
- package/local/llm-config.d.ts.map +1 -1
- package/local/llm-config.js +8 -14
- package/local/llm-config.js.map +1 -1
- package/local/setup/wizard.d.ts +4 -5
- package/local/setup/wizard.d.ts.map +1 -1
- package/local/setup/wizard.js +7 -9
- package/local/setup/wizard.js.map +1 -1
- package/local/state/startup-config.d.ts +5 -4
- package/local/state/startup-config.d.ts.map +1 -1
- package/local/state/startup-config.js +6 -3
- package/local/state/startup-config.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +5 -6
- package/local/status.js.map +1 -1
- package/package.json +5 -5
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +49 -2
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/declarative.d.ts +3 -3
- package/resources/apply/declarative.d.ts.map +1 -1
- package/resources/apply/declarative.js +28 -9
- package/resources/apply/declarative.js.map +1 -1
- package/resources/apply/handlers.d.ts +9 -0
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +1 -0
- package/resources/apply/handlers.js.map +1 -1
- package/resources/skill.d.ts +38 -0
- package/resources/skill.d.ts.map +1 -1
- package/resources/skill.js +84 -2
- package/resources/skill.js.map +1 -1
- package/src/commands/push.ts +74 -0
- package/src/commands/setup.ts +7 -4
- package/src/local/daemon/launch.ts +1 -6
- package/src/local/llm-config.test.ts +11 -14
- package/src/local/llm-config.ts +8 -15
- package/src/local/setup/wizard.test.ts +2 -3
- package/src/local/setup/wizard.ts +7 -9
- package/src/local/state/startup-config.ts +11 -7
- package/src/local/state/state.test.ts +23 -3
- package/src/local/status.test.ts +2 -2
- package/src/local/status.ts +5 -6
- package/src/resources/apply/apply.test.ts +134 -0
- package/src/resources/apply/apply.ts +63 -2
- package/src/resources/apply/declarative.integration.test.ts +79 -1
- package/src/resources/apply/declarative.ts +27 -10
- package/src/resources/apply/handlers.ts +10 -0
- package/src/resources/skill.test.ts +119 -5
- package/src/resources/skill.ts +108 -2
package/src/commands/push.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface PushFlags {
|
|
|
19
19
|
gitUrl?: string;
|
|
20
20
|
gitRef?: string;
|
|
21
21
|
subdir?: string;
|
|
22
|
+
archive?: string;
|
|
22
23
|
ignore?: string[];
|
|
23
24
|
include?: string[];
|
|
24
25
|
gitignore?: boolean; // false when --no-gitignore is passed (commander negation)
|
|
@@ -37,6 +38,7 @@ export function registerPush(program: Command): void {
|
|
|
37
38
|
.option("--git-url <url>", "push from a remote git repository URL")
|
|
38
39
|
.option("--git-ref <ref>", "git reference (tag, branch, or commit SHA) for remote push")
|
|
39
40
|
.option("--subdir <dir>", "subdirectory within the git repository containing the artifact")
|
|
41
|
+
.option("--archive <file>", "push a pre-packaged ZIP archive as-is (version hash = the file's SHA-256)")
|
|
40
42
|
.option("--ignore <pattern>", "additional pattern to ignore (repeatable)", collect, [])
|
|
41
43
|
.option("--include <pattern>", "pattern to force-include (repeatable)", collect, [])
|
|
42
44
|
.option("--no-gitignore", "don't respect .gitignore patterns")
|
|
@@ -72,6 +74,11 @@ async function runPush(type: string, path: string | undefined, options: PushFlag
|
|
|
72
74
|
const tag = options.tag ?? "latest";
|
|
73
75
|
const message = options.message ?? "";
|
|
74
76
|
|
|
77
|
+
if (options.archive !== undefined && options.archive !== "") {
|
|
78
|
+
await pushArchive(client.stigmer, skill, { org, tag, message, path, options });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
if (options.gitUrl !== undefined && options.gitUrl !== "") {
|
|
76
83
|
await pushRemote(client.stigmer, skill, { org, tag, message, ignoreOptions, decisionSink, options });
|
|
77
84
|
return;
|
|
@@ -95,6 +102,73 @@ async function runPush(type: string, path: string | undefined, options: PushFlag
|
|
|
95
102
|
process.stdout.write(renderResult(result, skill));
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
interface ArchiveContext {
|
|
106
|
+
org: string;
|
|
107
|
+
tag: string;
|
|
108
|
+
message: string;
|
|
109
|
+
path: string | undefined;
|
|
110
|
+
options: PushFlags;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Push a pre-packaged skill archive (`--archive`). The bytes are uploaded
|
|
115
|
+
* as-is, so source-mode and filtering flags have nothing to act on — passing
|
|
116
|
+
* one alongside --archive is a contradiction we fail loudly rather than
|
|
117
|
+
* silently ignore.
|
|
118
|
+
*/
|
|
119
|
+
async function pushArchive(
|
|
120
|
+
client: import("@stigmer/sdk").Stigmer,
|
|
121
|
+
skill: typeof import("../resources/skill.js"),
|
|
122
|
+
ctx: ArchiveContext,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const conflicts: string[] = [];
|
|
125
|
+
if (ctx.path !== undefined && ctx.path !== "") conflicts.push("[path]");
|
|
126
|
+
if (ctx.options.gitUrl !== undefined && ctx.options.gitUrl !== "") conflicts.push("--git-url");
|
|
127
|
+
if (ctx.options.gitRef !== undefined && ctx.options.gitRef !== "") conflicts.push("--git-ref");
|
|
128
|
+
if (ctx.options.subdir !== undefined && ctx.options.subdir !== "") conflicts.push("--subdir");
|
|
129
|
+
if ((ctx.options.ignore ?? []).length > 0) conflicts.push("--ignore");
|
|
130
|
+
if ((ctx.options.include ?? []).length > 0) conflicts.push("--include");
|
|
131
|
+
if (ctx.options.gitignore === false) conflicts.push("--no-gitignore");
|
|
132
|
+
if (conflicts.length > 0) {
|
|
133
|
+
throw new UsageError(
|
|
134
|
+
`--archive pushes a pre-packaged ZIP as-is and cannot be combined with: ${conflicts.join(", ")}\n\n` +
|
|
135
|
+
"Packaging already happened when the archive was built.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const archivePath = ctx.options.archive ?? "";
|
|
140
|
+
if (ctx.options.dryRun === true) {
|
|
141
|
+
process.stdout.write(renderArchiveDryRun(archivePath, skill.readSkillArchive(archivePath), skill));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await skill.pushSkillFromArchive(client, archivePath, ctx.org, ctx.tag, ctx.message);
|
|
146
|
+
process.stdout.write(renderResult(result, skill));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderArchiveDryRun(
|
|
150
|
+
archivePath: string,
|
|
151
|
+
archive: import("../resources/skill.js").SkillArchive,
|
|
152
|
+
skill: typeof import("../resources/skill.js"),
|
|
153
|
+
): string {
|
|
154
|
+
const lines = [
|
|
155
|
+
"",
|
|
156
|
+
`Dry run - validating skill archive: ${archivePath}`,
|
|
157
|
+
"",
|
|
158
|
+
`Skill name: ${archive.meta.name}`,
|
|
159
|
+
`Files: ${archive.fileCount}`,
|
|
160
|
+
`Content size: ${skill.formatBytes(archive.totalSize)}`,
|
|
161
|
+
`Upload size: ${skill.formatBytes(archive.bytes.length)}`,
|
|
162
|
+
"",
|
|
163
|
+
"The archive bytes are uploaded as-is: the registered version hash is the",
|
|
164
|
+
"SHA-256 of this file, so it matches any checksum you published for it.",
|
|
165
|
+
"",
|
|
166
|
+
"Run without --dry-run to push the skill artifact.",
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
return `${lines.join("\n")}\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
98
172
|
interface RemoteContext {
|
|
99
173
|
org: string;
|
|
100
174
|
tag: string;
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// `stigmer setup` — configure the LLM provider used for agent execution.
|
|
2
2
|
//
|
|
3
3
|
// Two paths share one decision core (local/setup/wizard.ts): a non-interactive
|
|
4
|
-
// flag path (`--provider`, `--api-key
|
|
4
|
+
// flag path (`--provider`, `--api-key`) for scripts and CI, and an
|
|
5
5
|
// interactive API-key prompt when no provider flag is given. Both write to
|
|
6
6
|
// backend.local.llm, preserving every other config field. The valid provider
|
|
7
7
|
// set is owned by wizard.ts's PROVIDER_CHOICES (Anthropic-only + skip — the
|
|
8
8
|
// only story the native runner serves).
|
|
9
|
+
//
|
|
10
|
+
// Setup deliberately has NO model concept: the platform model registry owns
|
|
11
|
+
// the execution default, and per-run overrides belong to `stigmer run
|
|
12
|
+
// --model`. A setup-level pin used to exist but never reached execution
|
|
13
|
+
// (oss#314) — config that looks authoritative but is dead invites exactly the
|
|
14
|
+
// bug class Session 29 fixed, so it is structurally gone rather than wired up.
|
|
9
15
|
|
|
10
16
|
import type { Command } from "commander";
|
|
11
17
|
import { homedir } from "node:os";
|
|
@@ -14,7 +20,6 @@ import { addResultFlags, resultFormat } from "./shared.js";
|
|
|
14
20
|
|
|
15
21
|
interface SetupFlags extends OutputFlags {
|
|
16
22
|
provider?: string;
|
|
17
|
-
model?: string;
|
|
18
23
|
apiKey?: string;
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -23,7 +28,6 @@ export function registerSetup(program: Command): void {
|
|
|
23
28
|
.command("setup")
|
|
24
29
|
.description("configure the LLM provider for agent execution")
|
|
25
30
|
.option("--provider <name>", "provider: anthropic, or skip to clear (non-interactive)")
|
|
26
|
-
.option("--model <name>", "pin a specific model (defaults to the platform-selected model)")
|
|
27
31
|
.option("--api-key <key>", "Anthropic API key")
|
|
28
32
|
.action((options: SetupFlags) => runSetup(options));
|
|
29
33
|
addResultFlags(setup);
|
|
@@ -47,7 +51,6 @@ async function runSetup(options: SetupFlags): Promise<void> {
|
|
|
47
51
|
]);
|
|
48
52
|
}
|
|
49
53
|
updated = applyChoice(config, provider as (typeof PROVIDER_CHOICES)[number], {
|
|
50
|
-
model: options.model,
|
|
51
54
|
apiKey: options.apiKey,
|
|
52
55
|
});
|
|
53
56
|
} else {
|
|
@@ -95,7 +95,7 @@ export async function up(options: UpOptions = {}, home: string = homedir()): Pro
|
|
|
95
95
|
|
|
96
96
|
await applySeedpackBestEffort(home);
|
|
97
97
|
|
|
98
|
-
saveStartupConfig(data, buildStartupConfig(data, logs, temporal.address,
|
|
98
|
+
saveStartupConfig(data, buildStartupConfig(data, logs, temporal.address, daemonPid, options));
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
@@ -246,18 +246,13 @@ function buildStartupConfig(
|
|
|
246
246
|
data: string,
|
|
247
247
|
logs: string,
|
|
248
248
|
temporalAddr: string,
|
|
249
|
-
config: Config,
|
|
250
249
|
daemonPid: number,
|
|
251
250
|
options: UpOptions,
|
|
252
251
|
): StartupConfig {
|
|
253
|
-
const local = config.backend.local as { llm?: { provider?: string; model?: string; base_url?: string } } | undefined;
|
|
254
252
|
return {
|
|
255
253
|
data_dir: data,
|
|
256
254
|
log_dir: logs,
|
|
257
255
|
temporal_addr: temporalAddr,
|
|
258
|
-
llm_provider: local?.llm?.provider ?? "",
|
|
259
|
-
llm_model: local?.llm?.model ?? "",
|
|
260
|
-
llm_base_url: local?.llm?.base_url ?? "",
|
|
261
256
|
execution_mode: "local",
|
|
262
257
|
sandbox_image: "",
|
|
263
258
|
sandbox_auto_pull: false,
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
detectProviderFromEnv,
|
|
5
5
|
readLlm,
|
|
6
6
|
resolveApiKey,
|
|
7
|
-
resolveModel,
|
|
8
7
|
resolveProvider,
|
|
9
8
|
setLlm,
|
|
10
9
|
} from "./llm-config.js";
|
|
@@ -32,17 +31,6 @@ describe("resolveProvider", () => {
|
|
|
32
31
|
});
|
|
33
32
|
});
|
|
34
33
|
|
|
35
|
-
describe("resolveModel", () => {
|
|
36
|
-
it("returns the explicit override, else empty — the registry owns the default", () => {
|
|
37
|
-
// No hardcoded model version anywhere in the CLI: unset means "platform
|
|
38
|
-
// default", resolved by the runner from the model registry.
|
|
39
|
-
expect(resolveModel(config({ llm: { provider: "anthropic" } }), {})).toBe("");
|
|
40
|
-
expect(resolveModel(config(), {})).toBe("");
|
|
41
|
-
expect(resolveModel(config({ llm: { provider: "anthropic", model: "claude-x" } }), {})).toBe("claude-x");
|
|
42
|
-
expect(resolveModel(config({ llm: { provider: "anthropic" } }), { STIGMER_LLM_MODEL: "envm" })).toBe("envm");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
34
|
describe("resolveApiKey", () => {
|
|
47
35
|
it("uses ANTHROPIC_API_KEY, then config", () => {
|
|
48
36
|
const cfg = config({ llm: { provider: "anthropic", api_key: "cfgkey" } });
|
|
@@ -58,12 +46,21 @@ describe("resolveApiKey", () => {
|
|
|
58
46
|
describe("setLlm", () => {
|
|
59
47
|
it("preserves sibling local keys and can clear the section", () => {
|
|
60
48
|
const cfg = config({ temporal: { managed: true }, llm: { provider: "anthropic" } });
|
|
61
|
-
const updated = setLlm(cfg, { provider: "anthropic",
|
|
62
|
-
expect(readLlm(updated)).toEqual({ provider: "anthropic",
|
|
49
|
+
const updated = setLlm(cfg, { provider: "anthropic", api_key: "k" });
|
|
50
|
+
expect(readLlm(updated)).toEqual({ provider: "anthropic", api_key: "k" });
|
|
63
51
|
expect((updated.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
|
|
64
52
|
|
|
65
53
|
const cleared = setLlm(cfg, undefined);
|
|
66
54
|
expect(readLlm(cleared)).toBeUndefined();
|
|
67
55
|
expect((cleared.backend.local as { temporal: unknown }).temporal).toEqual({ managed: true });
|
|
68
56
|
});
|
|
57
|
+
|
|
58
|
+
it("replaces the section wholesale — a stale model key from a pre-oss#314 config does not survive a rewrite", () => {
|
|
59
|
+
// The graceful-migration contract: old configs may carry llm.model (the
|
|
60
|
+
// removed dead pin). Reads ignore it (LlmSettings no longer models it);
|
|
61
|
+
// the next setup write clears it because setLlm never merges.
|
|
62
|
+
const cfg = config({ llm: { provider: "anthropic", model: "stale-pin", api_key: "k" } });
|
|
63
|
+
const rewritten = setLlm(cfg, { provider: "anthropic", api_key: "k" });
|
|
64
|
+
expect(readLlm(rewritten)).toEqual({ provider: "anthropic", api_key: "k" });
|
|
65
|
+
});
|
|
69
66
|
});
|
package/src/local/llm-config.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// The config module (config/config.ts) carries `backend.local` through verbatim
|
|
5
5
|
// so the CLI never drops fields it does not model. This module adds a *typed
|
|
6
6
|
// lens* for the one sub-tree setup owns — `local.llm` — and resolves the
|
|
7
|
-
// effective provider/
|
|
7
|
+
// effective provider/key with env-override precedence (env > config file >
|
|
8
8
|
// API-key autodetect). Writes merge into a shallow copy of `local`, preserving
|
|
9
9
|
// every sibling key (temporal, execution, …).
|
|
10
10
|
//
|
|
@@ -12,10 +12,13 @@
|
|
|
12
12
|
// Anthropic clients, and the platform model registry's native-harness entries
|
|
13
13
|
// are all Anthropic models. Two invariants follow:
|
|
14
14
|
//
|
|
15
|
-
// 1. The model registry — not this config — owns the
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// 1. The model registry — not this config — owns the execution model, so
|
|
16
|
+
// there is deliberately NO model field here. A config-level model pin
|
|
17
|
+
// existed once but never reached execution (oss#314): the launcher
|
|
18
|
+
// forwards only the API key, making such a pin config-that-lies. Per-run
|
|
19
|
+
// overrides belong to `stigmer run --model`. Stale `model` keys in
|
|
20
|
+
// existing config files are silently ignored (verbatim carry-through)
|
|
21
|
+
// and cleared by the next `setup` write.
|
|
19
22
|
// 2. `provider` exists to route the right API key to the runner, not to
|
|
20
23
|
// select an execution backend; "anthropic" is the only provider the local
|
|
21
24
|
// stack can serve.
|
|
@@ -25,7 +28,6 @@ import type { Config } from "../config/config.js";
|
|
|
25
28
|
/** LLM settings as persisted under `backend.local.llm` (snake_case = YAML keys). */
|
|
26
29
|
export interface LlmSettings {
|
|
27
30
|
provider?: string;
|
|
28
|
-
model?: string;
|
|
29
31
|
api_key?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -61,15 +63,6 @@ export function resolveProvider(config: Config, env: NodeJS.ProcessEnv = process
|
|
|
61
63
|
return detectProviderFromEnv(env);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
/**
|
|
65
|
-
* Effective model override: env override > config. Empty string means "no
|
|
66
|
-
* override" — the runner picks the default from the platform model registry.
|
|
67
|
-
*/
|
|
68
|
-
export function resolveModel(config: Config, env: NodeJS.ProcessEnv = process.env): string {
|
|
69
|
-
if (env.STIGMER_LLM_MODEL) return env.STIGMER_LLM_MODEL;
|
|
70
|
-
return readLlm(config)?.model ?? "";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
66
|
/** Effective API key: ANTHROPIC_API_KEY env var > config. */
|
|
74
67
|
export function resolveApiKey(config: Config, env: NodeJS.ProcessEnv = process.env): string {
|
|
75
68
|
if (resolveProvider(config, env) !== "anthropic") return "";
|
|
@@ -21,11 +21,10 @@ describe("PROVIDER_CHOICES", () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
describe("buildLlmForChoice", () => {
|
|
24
|
-
it("persists
|
|
24
|
+
it("persists provider and key only — there is no model concept, the registry owns it (oss#314)", () => {
|
|
25
25
|
expect(buildLlmForChoice("anthropic")).toEqual({ provider: "anthropic" });
|
|
26
|
-
expect(buildLlmForChoice("anthropic", { apiKey: "k"
|
|
26
|
+
expect(buildLlmForChoice("anthropic", { apiKey: "k" })).toEqual({
|
|
27
27
|
provider: "anthropic",
|
|
28
|
-
model: "m",
|
|
29
28
|
api_key: "k",
|
|
30
29
|
});
|
|
31
30
|
});
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// Local agent execution is Anthropic-only (the native runner only constructs
|
|
8
8
|
// Anthropic clients and the registry's native entries are Anthropic models),
|
|
9
9
|
// so the interactive flow is a single API-key prompt rather than a provider
|
|
10
|
-
// menu.
|
|
11
|
-
// owns the execution default, and
|
|
12
|
-
//
|
|
10
|
+
// menu. There is no model concept here at all: the platform model registry
|
|
11
|
+
// owns the execution default, and per-run overrides belong to `stigmer run
|
|
12
|
+
// --model` (oss#314 removed the dead setup-level pin).
|
|
13
13
|
|
|
14
14
|
import type { Config } from "../../config/config.js";
|
|
15
15
|
import { type LlmSettings, setLlm } from "../llm-config.js";
|
|
@@ -27,19 +27,17 @@ export type ProviderChoice = (typeof PROVIDER_CHOICES)[number];
|
|
|
27
27
|
|
|
28
28
|
export interface SelectionInputs {
|
|
29
29
|
apiKey?: string;
|
|
30
|
-
model?: string;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
|
-
* Resolve a provider choice (plus optional
|
|
35
|
-
* persist. "skip" yields `undefined`, which clears the section
|
|
36
|
-
* model
|
|
37
|
-
*
|
|
33
|
+
* Resolve a provider choice (plus an optional API key) to the LLM settings to
|
|
34
|
+
* persist. "skip" yields `undefined`, which clears the section — including any
|
|
35
|
+
* stale `model` key from a pre-oss#314 config, since setLlm replaces the
|
|
36
|
+
* section wholesale.
|
|
38
37
|
*/
|
|
39
38
|
export function buildLlmForChoice(choice: ProviderChoice, inputs: SelectionInputs = {}): LlmSettings | undefined {
|
|
40
39
|
if (choice === "skip") return undefined;
|
|
41
40
|
const settings: LlmSettings = { provider: choice };
|
|
42
|
-
if (inputs.model) settings.model = inputs.model;
|
|
43
41
|
if (inputs.apiKey) settings.api_key = inputs.apiKey;
|
|
44
42
|
return settings;
|
|
45
43
|
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the
|
|
3
|
-
//
|
|
1
|
+
// A diagnostic snapshot of the settings `up` launched with, persisted as
|
|
2
|
+
// `startup-config.json` in the data dir and removed by `down`. Nothing in the
|
|
3
|
+
// CLI or daemon reads it back — it exists for humans and support tooling
|
|
4
|
+
// inspecting a running stack, so keep it truthful: a field that no component
|
|
5
|
+
// consumes does not belong here (oss#314 removed the write-only llm_* trio).
|
|
6
|
+
// JSON keys stay snake_case for continuity with previously-written files.
|
|
4
7
|
|
|
5
8
|
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
9
|
import { join } from "node:path";
|
|
7
10
|
import { STARTUP_CONFIG_FILE } from "../constants.js";
|
|
8
11
|
|
|
9
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The persisted launch snapshot. `loadStartupConfig` parses leniently (a cast,
|
|
14
|
+
* not a validator), so files written by older CLIs with extra fields — e.g.
|
|
15
|
+
* the removed llm_* trio — load without migration.
|
|
16
|
+
*/
|
|
10
17
|
export interface StartupConfig {
|
|
11
18
|
data_dir: string;
|
|
12
19
|
log_dir: string;
|
|
13
20
|
temporal_addr: string;
|
|
14
|
-
llm_provider: string;
|
|
15
|
-
llm_model: string;
|
|
16
|
-
llm_base_url: string;
|
|
17
21
|
execution_mode: string;
|
|
18
22
|
sandbox_image: string;
|
|
19
23
|
sandbox_auto_pull: boolean;
|
|
@@ -82,9 +82,6 @@ describe("startup-config", () => {
|
|
|
82
82
|
data_dir: dir,
|
|
83
83
|
log_dir: join(dir, "logs"),
|
|
84
84
|
temporal_addr: "127.0.0.1:7233",
|
|
85
|
-
llm_provider: "anthropic",
|
|
86
|
-
llm_model: "claude",
|
|
87
|
-
llm_base_url: "",
|
|
88
85
|
execution_mode: "local",
|
|
89
86
|
sandbox_image: "",
|
|
90
87
|
sandbox_auto_pull: false,
|
|
@@ -98,6 +95,29 @@ describe("startup-config", () => {
|
|
|
98
95
|
removeStartupConfig(dir);
|
|
99
96
|
expect(loadStartupConfig(dir)).toBeNull();
|
|
100
97
|
});
|
|
98
|
+
|
|
99
|
+
it("loads files written by older CLIs carrying since-removed fields (e.g. the llm_* trio)", () => {
|
|
100
|
+
// The lenient-load contract oss#314 leans on: no migration for on-disk
|
|
101
|
+
// files — extra keys simply come along and nothing consumes them.
|
|
102
|
+
const dir = tempDir("stigmer-startup-");
|
|
103
|
+
const legacy = {
|
|
104
|
+
data_dir: dir,
|
|
105
|
+
log_dir: join(dir, "logs"),
|
|
106
|
+
temporal_addr: "127.0.0.1:7233",
|
|
107
|
+
llm_provider: "anthropic",
|
|
108
|
+
llm_model: "claude",
|
|
109
|
+
llm_base_url: "",
|
|
110
|
+
execution_mode: "local",
|
|
111
|
+
sandbox_image: "",
|
|
112
|
+
sandbox_auto_pull: false,
|
|
113
|
+
sandbox_cleanup: true,
|
|
114
|
+
sandbox_ttl: 0,
|
|
115
|
+
stigmer_server_pid: 999,
|
|
116
|
+
server_only: false,
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(join(dir, "startup-config.json"), JSON.stringify(legacy));
|
|
119
|
+
expect(loadStartupConfig(dir)).toMatchObject({ temporal_addr: "127.0.0.1:7233", stigmer_server_pid: 999 });
|
|
120
|
+
});
|
|
101
121
|
});
|
|
102
122
|
|
|
103
123
|
describe("log-rotation", () => {
|
package/src/local/status.test.ts
CHANGED
|
@@ -62,12 +62,12 @@ describe("buildStatusResult", () => {
|
|
|
62
62
|
expect(field(result, "Web UI", "Temporal")).toBe("http://localhost:8233");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("
|
|
65
|
+
it("ignores a stale model key from a pre-oss#314 config — the pin never reached execution, so status must not display it", async () => {
|
|
66
66
|
writeHealth({ daemon_pid: process.pid, started_at: new Date().toISOString(), components: {} });
|
|
67
67
|
writeFileSync(configPath(home), "backend:\n type: local\n local:\n llm:\n provider: anthropic\n model: claude-x\n api_key: sk-x\n");
|
|
68
68
|
|
|
69
69
|
const result = await buildStatusResult(home, open);
|
|
70
|
-
expect(field(result, "LLM Configuration", "Model")).toBe("
|
|
70
|
+
expect(field(result, "LLM Configuration", "Model")).toBe("Auto (platform default)");
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it("reports a non-anthropic provider as unknown — local execution is Anthropic-only", async () => {
|
package/src/local/status.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { load as loadConfig } from "../config/config.js";
|
|
|
13
13
|
import { configPath } from "../config/paths.js";
|
|
14
14
|
import { CommandResult } from "../output/command-result.js";
|
|
15
15
|
import { SERVER_PORT, TEMPORAL_UI_PORT, WEB_CONSOLE_PORT } from "./constants.js";
|
|
16
|
-
import { resolveApiKey,
|
|
16
|
+
import { resolveApiKey, resolveProvider } from "./llm-config.js";
|
|
17
17
|
import { tcpConnects } from "./net/tcp.js";
|
|
18
18
|
import { dataDir } from "./paths.js";
|
|
19
19
|
import { type ComponentState, type HealthState, loadHealthState } from "./state/health-state.js";
|
|
@@ -130,12 +130,11 @@ function addLlmSection(result: CommandResult, home: string): void {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
//
|
|
134
|
-
// execution default
|
|
135
|
-
//
|
|
136
|
-
const model = resolveModel(config);
|
|
133
|
+
// The model line is a constant: the platform model registry picks the
|
|
134
|
+
// execution default and the CLI has no model concept to report (oss#314
|
|
135
|
+
// removed the dead config pin). Per-run overrides ride `stigmer run --model`.
|
|
137
136
|
section.field("Provider", "Anthropic (Cloud)");
|
|
138
|
-
section.field("Model",
|
|
137
|
+
section.field("Model", "Auto (platform default)");
|
|
139
138
|
section.field("API Key", resolveApiKey(config) !== "" ? "Configured ✓" : "Not configured ✗");
|
|
140
139
|
}
|
|
141
140
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Unit tests for the shared apply core's declared-visibility follow-up
|
|
2
|
+
// (oss#573). Plain updates preserve stored visibility on both editions, so
|
|
3
|
+
// the apply RPC's response carries the STORED level; when a manifest
|
|
4
|
+
// declares a different one, the core must land it through the guarded
|
|
5
|
+
// updateVisibility RPC — or warn when the kind has no such door.
|
|
6
|
+
|
|
7
|
+
import { create, type Message } from "@bufbuild/protobuf";
|
|
8
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
9
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
10
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
11
|
+
import type { UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { applyMessage } from "./apply.js";
|
|
14
|
+
import type { ApplyHandler, ControllerFn } from "./handlers.js";
|
|
15
|
+
|
|
16
|
+
// The controller accessor is only ever forwarded to handler methods, which
|
|
17
|
+
// these tests stub out — a throwing dummy proves nothing else touches it.
|
|
18
|
+
const controller: ControllerFn = () => {
|
|
19
|
+
throw new Error("unexpected controller access");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function agent(visibility: ApiResourceVisibility, id = "agent-1") {
|
|
23
|
+
return create(AgentSchema, {
|
|
24
|
+
metadata: { id, name: "a", org: "acme", visibility },
|
|
25
|
+
spec: { instructions: "i" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HandlerOptions {
|
|
30
|
+
applyReturns: Message;
|
|
31
|
+
updateVisibility?: (input: UpdateVisibilityInput) => Promise<Message>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handlerWith(opts: HandlerOptions): { handler: ApplyHandler; calls: UpdateVisibilityInput[] } {
|
|
35
|
+
const calls: UpdateVisibilityInput[] = [];
|
|
36
|
+
const handler: ApplyHandler = {
|
|
37
|
+
kind: ApiResourceKind.agent,
|
|
38
|
+
displayName: "Agent",
|
|
39
|
+
schema: AgentSchema,
|
|
40
|
+
applyOrder: 3,
|
|
41
|
+
apply: () => Promise.resolve(opts.applyReturns),
|
|
42
|
+
...(opts.updateVisibility !== undefined && {
|
|
43
|
+
updateVisibility: (_c: ControllerFn, input: UpdateVisibilityInput) => {
|
|
44
|
+
calls.push(input);
|
|
45
|
+
return opts.updateVisibility!(input);
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
return { handler, calls };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("applyMessage declared-visibility follow-up", () => {
|
|
53
|
+
it("lands a declared level the update preserved away, and reflects it on the outcome", async () => {
|
|
54
|
+
// Server preserved stored org; manifest declares public.
|
|
55
|
+
const { handler, calls } = handlerWith({
|
|
56
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
57
|
+
updateVisibility: () => Promise.resolve(agent(ApiResourceVisibility.visibility_public)),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
61
|
+
|
|
62
|
+
expect(calls).toHaveLength(1);
|
|
63
|
+
expect(calls[0].resourceId).toBe("agent-1");
|
|
64
|
+
expect(calls[0].visibility).toBe(ApiResourceVisibility.visibility_public);
|
|
65
|
+
expect(outcome.warning).toBeUndefined();
|
|
66
|
+
const appliedMeta = (outcome.applied as { metadata?: { visibility?: ApiResourceVisibility } })?.metadata;
|
|
67
|
+
expect(appliedMeta?.visibility).toBe(ApiResourceVisibility.visibility_public);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("skips the follow-up when the manifest omits visibility", async () => {
|
|
71
|
+
const { handler, calls } = handlerWith({
|
|
72
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
73
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const outcome = await applyMessage(
|
|
77
|
+
controller,
|
|
78
|
+
handler,
|
|
79
|
+
agent(ApiResourceVisibility.api_resource_visibility_unspecified),
|
|
80
|
+
"acme",
|
|
81
|
+
false,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(calls).toHaveLength(0);
|
|
85
|
+
expect(outcome.warning).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("skips the follow-up when the server already matches (idempotent re-apply)", async () => {
|
|
89
|
+
const { handler, calls } = handlerWith({
|
|
90
|
+
applyReturns: agent(ApiResourceVisibility.visibility_public),
|
|
91
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
95
|
+
|
|
96
|
+
expect(calls).toHaveLength(0);
|
|
97
|
+
expect(outcome.warning).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("warns instead of silently swallowing a diff on kinds without the RPC", async () => {
|
|
101
|
+
const { handler } = handlerWith({
|
|
102
|
+
applyReturns: agent(ApiResourceVisibility.visibility_private),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
106
|
+
|
|
107
|
+
expect(outcome.warning).toMatch(/visibility cannot be changed declaratively/);
|
|
108
|
+
expect(outcome.warning).toMatch(/stored value is kept/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("fails loudly when the guarded door rejects, naming the partial state", async () => {
|
|
112
|
+
// e.g. the default-instance FAILED_PRECONDITION or an unsupported level.
|
|
113
|
+
const { handler } = handlerWith({
|
|
114
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
115
|
+
updateVisibility: () => Promise.reject(new Error("default instances do not have their own visibility")),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false),
|
|
120
|
+
).rejects.toThrow(/spec applied, but the manifest's visibility change was rejected/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not follow up in dry-run mode", async () => {
|
|
124
|
+
const { handler, calls } = handlerWith({
|
|
125
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
126
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", true);
|
|
130
|
+
|
|
131
|
+
expect(calls).toHaveLength(0);
|
|
132
|
+
expect(outcome.applied).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|