@stigmer/cli 3.12.4 → 3.12.5
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/draft.js +4 -2
- package/commands/draft.js.map +1 -1
- package/commands/run.js +4 -2
- package/commands/run.js.map +1 -1
- package/commands/seedpack.d.ts.map +1 -1
- package/commands/seedpack.js +27 -5
- package/commands/seedpack.js.map +1 -1
- package/local/seedpack/apply.d.ts +32 -3
- package/local/seedpack/apply.d.ts.map +1 -1
- package/local/seedpack/apply.js +59 -8
- package/local/seedpack/apply.js.map +1 -1
- package/package.json +5 -5
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +10 -1
- package/resources/apply/apply.js.map +1 -1
- package/resources/connect/connect.d.ts +7 -0
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +47 -30
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/display.d.ts.map +1 -1
- package/resources/connect/display.js +3 -0
- package/resources/connect/display.js.map +1 -1
- package/resources/run/prepare.d.ts +12 -1
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +27 -2
- package/resources/run/prepare.js.map +1 -1
- package/resources/task-configs.d.ts +15 -0
- package/resources/task-configs.d.ts.map +1 -0
- package/resources/task-configs.js +135 -0
- package/resources/task-configs.js.map +1 -0
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +8 -1
- package/resources/validate.js.map +1 -1
- package/src/commands/draft.ts +4 -2
- package/src/commands/run.ts +4 -2
- package/src/commands/seedpack.ts +27 -5
- package/src/local/seedpack/apply.test.ts +127 -6
- package/src/local/seedpack/apply.ts +69 -9
- package/src/resources/apply/apply.ts +11 -1
- package/src/resources/connect/connect.ts +70 -44
- package/src/resources/connect/display.ts +3 -0
- package/src/resources/run/prepare.test.ts +80 -0
- package/src/resources/run/prepare.ts +41 -1
- package/src/resources/task-configs.test.ts +98 -0
- package/src/resources/task-configs.ts +150 -0
- package/src/resources/validate.test.ts +26 -0
- package/src/resources/validate.ts +10 -2
|
@@ -65,6 +65,18 @@ export interface PreparedRun {
|
|
|
65
65
|
readonly serviceTier: ServiceTierFlag;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Optional behavior switches for {@link prepareAgentExec}. */
|
|
69
|
+
export interface PrepareAgentExecOptions {
|
|
70
|
+
/**
|
|
71
|
+
* When `true` (the caller's backend is Stigmer Cloud) and `--model` is
|
|
72
|
+
* omitted, the model is filled from the caller's account preference
|
|
73
|
+
* (`IdentityAccountPreferences.default_native_model`) via `whoAmI()`.
|
|
74
|
+
* Local mode has no IdentityAccount, so callers pass `false` there and
|
|
75
|
+
* the omitted model keeps resolving to the platform default.
|
|
76
|
+
*/
|
|
77
|
+
readonly cloudBackend?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
/**
|
|
69
81
|
* Validate flags and resolve all execution inputs. The `org` is used to inject
|
|
70
82
|
* STIGMER_ORG_ID and the `client` to upload non-workspace attachments.
|
|
@@ -74,11 +86,22 @@ export async function prepareAgentExec(
|
|
|
74
86
|
client: Stigmer,
|
|
75
87
|
org: string,
|
|
76
88
|
progress?: ProgressSink,
|
|
89
|
+
options?: PrepareAgentExecOptions,
|
|
77
90
|
): Promise<PreparedRun> {
|
|
78
91
|
const defaultAction = parseApprovalAction(flags.approveDefault);
|
|
79
92
|
validateMode(flags.mode);
|
|
80
93
|
validateServiceTier(flags.serviceTier);
|
|
81
94
|
|
|
95
|
+
// Layered model seed (oss#293 Phase 1.5): an explicit --model always wins;
|
|
96
|
+
// an omitted one fills from the account preference on cloud. `run` and
|
|
97
|
+
// `draft` always create a NEW session (threading lives in `resume`, which
|
|
98
|
+
// does not pass through here), so the fill never injects a native model
|
|
99
|
+
// into an existing cursor-harness session.
|
|
100
|
+
const model =
|
|
101
|
+
flags.model === "" && options?.cloudBackend === true
|
|
102
|
+
? await resolveModelFromAccountPreference(client)
|
|
103
|
+
: flags.model;
|
|
104
|
+
|
|
82
105
|
const workspaceEntries = parseWorkspaceEntries(flags.workspace, flags.branch, flags.commit);
|
|
83
106
|
|
|
84
107
|
const runtimeEnv = loadAndMergeEnv({
|
|
@@ -107,13 +130,30 @@ export async function prepareAgentExec(
|
|
|
107
130
|
message: flags.message,
|
|
108
131
|
detach: flags.detach,
|
|
109
132
|
verbose: flags.verbose,
|
|
110
|
-
model
|
|
133
|
+
model,
|
|
111
134
|
autoApproveAll: flags.autoApprove,
|
|
112
135
|
mode: flags.mode,
|
|
113
136
|
serviceTier: flags.serviceTier,
|
|
114
137
|
};
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Best-effort read of the caller's default model for native-harness runs
|
|
142
|
+
* (CLI sessions are native — there is no --harness flag yet, so the cursor
|
|
143
|
+
* default is deliberately not consulted). Any failure — network, auth, a
|
|
144
|
+
* backend without IdentityAccount — resolves to "" and the run proceeds
|
|
145
|
+
* exactly as an unfilled --model does today: a missing preference must
|
|
146
|
+
* never fail a run.
|
|
147
|
+
*/
|
|
148
|
+
async function resolveModelFromAccountPreference(client: Stigmer): Promise<string> {
|
|
149
|
+
try {
|
|
150
|
+
const account = await client.identityAccount.whoAmI();
|
|
151
|
+
return account.spec?.preferences?.defaultNativeModel ?? "";
|
|
152
|
+
} catch {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
/**
|
|
118
158
|
* Parse the `--approve-default` flag to an ApprovalAction. Empty means "no
|
|
119
159
|
* default" (UNSPECIFIED). Mirrors Go's approval.ParseAction, including the
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import { type Workflow, WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
3
|
+
import {
|
|
4
|
+
WorkflowTaskKind,
|
|
5
|
+
WorkflowTaskKindSchema,
|
|
6
|
+
} from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { decodeWorkflowTaskConfigs, TASK_CONFIG_SCHEMAS } from "./task-configs.js";
|
|
9
|
+
|
|
10
|
+
function makeWorkflow(tasks: JsonValue): Workflow {
|
|
11
|
+
return fromJson(WorkflowSchema, {
|
|
12
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
13
|
+
kind: "Workflow",
|
|
14
|
+
metadata: { name: "decode-test", org: "acme" },
|
|
15
|
+
spec: {
|
|
16
|
+
description: "task-config decode fixture",
|
|
17
|
+
document: { dsl: "1.0.0", namespace: "acme", name: "decode-test", version: "1.0.0" },
|
|
18
|
+
tasks,
|
|
19
|
+
},
|
|
20
|
+
}) as Workflow;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("TASK_CONFIG_SCHEMAS", () => {
|
|
24
|
+
// The stigmer/stigmer#353 drift class: hold the map and the proto enum to
|
|
25
|
+
// strict bidirectional equality, so a new task kind cannot ship without a
|
|
26
|
+
// CLI schema binding and a retired kind cannot linger in the map. The
|
|
27
|
+
// server-side twin of this promise is unmarshalTaskConfig's kind switch.
|
|
28
|
+
it("covers every WorkflowTaskKind exactly (bidirectional)", () => {
|
|
29
|
+
const declaredKinds = WorkflowTaskKindSchema.values
|
|
30
|
+
.map((v) => v.number as WorkflowTaskKind)
|
|
31
|
+
.filter((n) => n !== WorkflowTaskKind.workflow_task_kind_unspecified)
|
|
32
|
+
.sort((a, b) => a - b);
|
|
33
|
+
const mappedKinds = [...TASK_CONFIG_SCHEMAS.keys()].sort((a, b) => a - b);
|
|
34
|
+
expect(mappedKinds).toEqual(declaredKinds);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("decodeWorkflowTaskConfigs", () => {
|
|
39
|
+
it("rejects the string duration form a real apply rejects (the #778 repro)", () => {
|
|
40
|
+
const workflow = makeWorkflow([
|
|
41
|
+
{ name: "conditional_wait", kind: "wait", task_config: { duration: "5s" } },
|
|
42
|
+
]);
|
|
43
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).toThrow(
|
|
44
|
+
/task 'conditional_wait' \(wait\): invalid task_config/,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("accepts the typed duration object", () => {
|
|
49
|
+
const workflow = makeWorkflow([
|
|
50
|
+
{ name: "cooldown", kind: "wait", task_config: { duration: { seconds: 5 } } },
|
|
51
|
+
]);
|
|
52
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rejects unknown task_config fields, mirroring the server's strict decode", () => {
|
|
56
|
+
const workflow = makeWorkflow([
|
|
57
|
+
{
|
|
58
|
+
name: "gate",
|
|
59
|
+
kind: "human_input",
|
|
60
|
+
task_config: { prompt: "Approve?", escalation_task: "somewhere" },
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).toThrow(/gate.*human_input.*task_config/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects values outside an enum's membership", () => {
|
|
67
|
+
const workflow = makeWorkflow([
|
|
68
|
+
{
|
|
69
|
+
name: "gate",
|
|
70
|
+
kind: "human_input",
|
|
71
|
+
task_config: { prompt: "Approve?", timeout: 60, on_timeout: "sometimes" },
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).toThrow(/gate.*invalid task_config/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("accepts the agent_call DSL shorthands a real apply accepts (never stricter)", () => {
|
|
78
|
+
const workflow = makeWorkflow([
|
|
79
|
+
{
|
|
80
|
+
name: "review",
|
|
81
|
+
kind: "agent_call",
|
|
82
|
+
task_config: {
|
|
83
|
+
agent: "acme/code-reviewer",
|
|
84
|
+
message: "Review ${ $context.pr.diff }",
|
|
85
|
+
harness: "cursor",
|
|
86
|
+
run_config: { model_name: "composer-2.5", service_tier: "fast" },
|
|
87
|
+
environment_refs: [{ slug: "shared-secrets" }],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("skips tasks without a task_config (protovalidate territory, not decode truth)", () => {
|
|
95
|
+
const workflow = makeWorkflow([{ name: "bare", kind: "set_vars" }]);
|
|
96
|
+
expect(() => decodeWorkflowTaskConfigs(workflow)).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Typed decode of workflow task_config blocks — the offline twin of the
|
|
2
|
+
// server's `unmarshalTaskConfig` (backend/services/stigmer-server/pkg/domain/
|
|
3
|
+
// workflow/converter/unmarshal.go).
|
|
4
|
+
//
|
|
5
|
+
// The Workflow proto models `task_config` as an open google.protobuf.Struct
|
|
6
|
+
// discriminated by `kind`, so the top-level schema decode accepts ANY config
|
|
7
|
+
// payload; a real apply then decodes it into the kind's typed message and
|
|
8
|
+
// rejects what does not fit. Before this module, `stigmer validate` and
|
|
9
|
+
// `apply --dry-run` reported "valid" for configs a real apply refuses
|
|
10
|
+
// (stigmer/stigmer#778).
|
|
11
|
+
//
|
|
12
|
+
// Scope is decode-truth only: field shapes, types, enum membership, unknown
|
|
13
|
+
// fields. Semantic validation (cross-task references, expression namespaces,
|
|
14
|
+
// model registry, protovalidate required-field rules) remains the server's
|
|
15
|
+
// authority via `validateSpec`.
|
|
16
|
+
|
|
17
|
+
import { type DescMessage, fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
18
|
+
import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
19
|
+
import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
|
|
20
|
+
import { AgentCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/agent_call_pb";
|
|
21
|
+
import { CallActivityTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/call_activity_pb";
|
|
22
|
+
import { EmitEventTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/emit_event_pb";
|
|
23
|
+
import { EvalTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/eval_pb";
|
|
24
|
+
import { ForTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/for_pb";
|
|
25
|
+
import { ForkTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/fork_pb";
|
|
26
|
+
import { GrpcCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/grpc_call_pb";
|
|
27
|
+
import { HttpCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/http_call_pb";
|
|
28
|
+
import { HumanInputTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/human_input_pb";
|
|
29
|
+
import { ListenTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/listen_pb";
|
|
30
|
+
import { LlmCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/llm_call_pb";
|
|
31
|
+
import { NotificationTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/notification_pb";
|
|
32
|
+
import { RaiseTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/raise_pb";
|
|
33
|
+
import { RunTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/run_pb";
|
|
34
|
+
import { SetTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/set_pb";
|
|
35
|
+
import { SwitchTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/switch_pb";
|
|
36
|
+
import { TransformTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/transform_pb";
|
|
37
|
+
import { TryTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/try_pb";
|
|
38
|
+
import { ValidateTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/validate_pb";
|
|
39
|
+
import { WaitTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/wait_pb";
|
|
40
|
+
|
|
41
|
+
// Exported for the drift test (task-configs.test.ts), which holds this map and
|
|
42
|
+
// the WorkflowTaskKind enum descriptor to strict bidirectional equality — the
|
|
43
|
+
// stigmer/stigmer#353 drift class: a new task kind cannot ship without a CLI
|
|
44
|
+
// schema binding, and a retired kind cannot linger here. The entries mirror
|
|
45
|
+
// the server's `unmarshalTaskConfig` kind switch one-for-one.
|
|
46
|
+
export const TASK_CONFIG_SCHEMAS: ReadonlyMap<WorkflowTaskKind, DescMessage> = new Map<
|
|
47
|
+
WorkflowTaskKind,
|
|
48
|
+
DescMessage
|
|
49
|
+
>([
|
|
50
|
+
[WorkflowTaskKind.set_vars, SetTaskConfigSchema],
|
|
51
|
+
[WorkflowTaskKind.http_call, HttpCallTaskConfigSchema],
|
|
52
|
+
[WorkflowTaskKind.grpc_call, GrpcCallTaskConfigSchema],
|
|
53
|
+
[WorkflowTaskKind.switch_case, SwitchTaskConfigSchema],
|
|
54
|
+
[WorkflowTaskKind.for_each, ForTaskConfigSchema],
|
|
55
|
+
[WorkflowTaskKind.fork, ForkTaskConfigSchema],
|
|
56
|
+
[WorkflowTaskKind.try_catch, TryTaskConfigSchema],
|
|
57
|
+
[WorkflowTaskKind.listen, ListenTaskConfigSchema],
|
|
58
|
+
[WorkflowTaskKind.wait, WaitTaskConfigSchema],
|
|
59
|
+
[WorkflowTaskKind.activity_call, CallActivityTaskConfigSchema],
|
|
60
|
+
[WorkflowTaskKind.raise_error, RaiseTaskConfigSchema],
|
|
61
|
+
[WorkflowTaskKind.run_workflow, RunTaskConfigSchema],
|
|
62
|
+
[WorkflowTaskKind.agent_call, AgentCallTaskConfigSchema],
|
|
63
|
+
[WorkflowTaskKind.llm_call, LlmCallTaskConfigSchema],
|
|
64
|
+
[WorkflowTaskKind.transform, TransformTaskConfigSchema],
|
|
65
|
+
[WorkflowTaskKind.human_input, HumanInputTaskConfigSchema],
|
|
66
|
+
[WorkflowTaskKind.validate, ValidateTaskConfigSchema],
|
|
67
|
+
[WorkflowTaskKind.emit_event, EmitEventTaskConfigSchema],
|
|
68
|
+
[WorkflowTaskKind.notification, NotificationTaskConfigSchema],
|
|
69
|
+
[WorkflowTaskKind.eval, EvalTaskConfigSchema],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Decodes every task's `task_config` into its kind's typed proto message,
|
|
74
|
+
* throwing on the first config a real apply would reject. Runs on the decoded
|
|
75
|
+
* Workflow message (after the top-level schema decode), so snake_case and
|
|
76
|
+
* json-name spellings are already normalized.
|
|
77
|
+
*
|
|
78
|
+
* A task with no `task_config` is skipped: its absence is a protovalidate
|
|
79
|
+
* required-field violation, which is the server's layer, not decode-truth.
|
|
80
|
+
*/
|
|
81
|
+
export function decodeWorkflowTaskConfigs(workflow: Workflow): void {
|
|
82
|
+
for (const task of workflow.spec?.tasks ?? []) {
|
|
83
|
+
if (task.taskConfig === undefined) continue;
|
|
84
|
+
const schema = TASK_CONFIG_SCHEMAS.get(task.kind);
|
|
85
|
+
if (schema === undefined) continue; // unspecified/unknown kind — the server's layer reports it
|
|
86
|
+
|
|
87
|
+
// protobuf-es represents google.protobuf.Struct fields as plain JSON, so
|
|
88
|
+
// the Struct's content feeds the per-kind decode directly.
|
|
89
|
+
const config = normalizeTaskConfigShorthands(task.kind, task.taskConfig);
|
|
90
|
+
try {
|
|
91
|
+
fromJson(schema, config, { ignoreUnknownFields: false });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`task '${task.name}' (${WorkflowTaskKind[task.kind]}): invalid task_config: ${(err as Error).message}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Rewrites user-friendly DSL forms to the shapes the proto decode expects —
|
|
102
|
+
* the twin of the server's `normalizeEnumShorthands` (unmarshal.go): agent_call
|
|
103
|
+
* harness shorthands ("cursor" → "HARNESS_CURSOR"), run_config service-tier
|
|
104
|
+
* shorthands ("fast" → "SERVICE_TIER_FAST"), and the environment_refs kind
|
|
105
|
+
* default (an omitted kind means environment). Without this twin the offline
|
|
106
|
+
* decode would be STRICTER than a real apply and fail valid manifests.
|
|
107
|
+
*/
|
|
108
|
+
function normalizeTaskConfigShorthands(kind: WorkflowTaskKind, config: JsonValue): JsonValue {
|
|
109
|
+
if (kind !== WorkflowTaskKind.agent_call || !isJsonObject(config)) {
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalized: Record<string, JsonValue> = { ...config };
|
|
114
|
+
|
|
115
|
+
if (typeof normalized.harness === "string") {
|
|
116
|
+
const harness = HARNESS_SHORTHANDS[normalized.harness.toLowerCase()];
|
|
117
|
+
if (harness !== undefined) normalized.harness = harness;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isJsonObject(normalized.run_config)) {
|
|
121
|
+
const runConfig: Record<string, JsonValue> = { ...normalized.run_config };
|
|
122
|
+
if (typeof runConfig.service_tier === "string") {
|
|
123
|
+
const tier = SERVICE_TIER_SHORTHANDS[runConfig.service_tier.toLowerCase()];
|
|
124
|
+
if (tier !== undefined) runConfig.service_tier = tier;
|
|
125
|
+
}
|
|
126
|
+
normalized.run_config = runConfig;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(normalized.environment_refs)) {
|
|
130
|
+
normalized.environment_refs = normalized.environment_refs.map((ref) =>
|
|
131
|
+
isJsonObject(ref) && ref.kind === undefined ? { ...ref, kind: "environment" } : ref,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const HARNESS_SHORTHANDS: Record<string, string> = {
|
|
139
|
+
native: "HARNESS_NATIVE",
|
|
140
|
+
cursor: "HARNESS_CURSOR",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const SERVICE_TIER_SHORTHANDS: Record<string, string> = {
|
|
144
|
+
standard: "SERVICE_TIER_STANDARD",
|
|
145
|
+
fast: "SERVICE_TIER_FAST",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function isJsonObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
150
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from "@bufbuild/protobuf";
|
|
1
2
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
3
4
|
import { schemaForValidate, validateDocument } from "./validate.js";
|
|
@@ -51,4 +52,29 @@ describe("validateDocument", () => {
|
|
|
51
52
|
}),
|
|
52
53
|
).toThrow();
|
|
53
54
|
});
|
|
55
|
+
|
|
56
|
+
// The stigmer/stigmer#778 wiring pin: task_config blocks are typed per kind,
|
|
57
|
+
// so validate rejects what a real apply rejects instead of waving any Struct
|
|
58
|
+
// payload through. Decode behavior itself is pinned in task-configs.test.ts.
|
|
59
|
+
it("decodes workflow task_config blocks per kind", () => {
|
|
60
|
+
const workflowSchema = schemaForValidate(ApiResourceKind.workflow);
|
|
61
|
+
if (workflowSchema === undefined) throw new Error("workflow schema unavailable");
|
|
62
|
+
|
|
63
|
+
const workflowDoc = (taskConfig: JsonValue): JsonValue => ({
|
|
64
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
65
|
+
kind: "Workflow",
|
|
66
|
+
metadata: { name: "wf", org: "acme" },
|
|
67
|
+
spec: {
|
|
68
|
+
document: { dsl: "1.0.0", namespace: "acme", name: "wf", version: "1.0.0" },
|
|
69
|
+
tasks: [{ name: "conditional_wait", kind: "wait", task_config: taskConfig }],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(() => validateDocument(workflowSchema, workflowDoc({ duration: "5s" }))).toThrow(
|
|
74
|
+
/conditional_wait.*invalid task_config/,
|
|
75
|
+
);
|
|
76
|
+
expect(() =>
|
|
77
|
+
validateDocument(workflowSchema, workflowDoc({ duration: { seconds: 5 } })),
|
|
78
|
+
).not.toThrow();
|
|
79
|
+
});
|
|
54
80
|
});
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
import { type DescMessage, fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
9
9
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
10
10
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
11
|
-
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
11
|
+
import { type Workflow, WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
12
12
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
13
13
|
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
14
|
+
import { decodeWorkflowTaskConfigs } from "./task-configs.js";
|
|
14
15
|
|
|
15
16
|
// Exported for the verb/dispatch conformance suite (registry/registry.test.ts),
|
|
16
17
|
// which holds this map and the matrix's Verb.Validate promises to strict
|
|
@@ -31,5 +32,12 @@ export function schemaForValidate(kind: ApiResourceKind): DescMessage | undefine
|
|
|
31
32
|
export function validateDocument(schema: DescMessage, document: JsonValue): void {
|
|
32
33
|
// ignoreUnknownFields mirrors the server's lenient unmarshal: forward-compat
|
|
33
34
|
// fields a newer server adds should not fail a slightly older CLI.
|
|
34
|
-
fromJson(schema, document, { ignoreUnknownFields: true });
|
|
35
|
+
const message = fromJson(schema, document, { ignoreUnknownFields: true });
|
|
36
|
+
|
|
37
|
+
// Workflows carry per-kind typed configs inside an open Struct that the
|
|
38
|
+
// top-level decode cannot see into — decode them too, so validate stops
|
|
39
|
+
// passing configs a real apply rejects (stigmer/stigmer#778).
|
|
40
|
+
if (schema === WorkflowSchema) {
|
|
41
|
+
decodeWorkflowTaskConfigs(message as Workflow);
|
|
42
|
+
}
|
|
35
43
|
}
|