@soat/cli 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +413 -110
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
2
|
import { createServer } from "node:http";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,13 +10,20 @@ import { program } from "commander";
|
|
|
11
10
|
import * as fs from "node:fs";
|
|
12
11
|
import { load } from "js-yaml";
|
|
13
12
|
import * as os from "node:os";
|
|
13
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
14
14
|
|
|
15
15
|
//#region package.json
|
|
16
|
-
var version = "0.
|
|
16
|
+
var version = "0.24.0";
|
|
17
17
|
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/cli-wrappers/wrappers/formations.ts
|
|
20
20
|
var FORMATION_COMMANDS = ["validate-formation", "plan-formation", "create-formation", "update-formation"];
|
|
21
|
+
/**
|
|
22
|
+
* The two commands that *deploy*. `validate-formation` and `plan-formation`
|
|
23
|
+
* report on a template without touching a resource, so their outcome is the
|
|
24
|
+
* payload, not an exit code.
|
|
25
|
+
*/
|
|
26
|
+
var DEPLOY_COMMANDS = ["create-formation", "update-formation"];
|
|
21
27
|
var TEMPLATE_PATH_FLAG = "template-path";
|
|
22
28
|
var TEMPLATE_FILE_FLAG = "template-file";
|
|
23
29
|
var ENV_FILE_FLAG = "env-file";
|
|
@@ -121,9 +127,45 @@ var resolveParameterPair = args => {
|
|
|
121
127
|
})
|
|
122
128
|
};
|
|
123
129
|
};
|
|
130
|
+
var isRecord = value => {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
};
|
|
133
|
+
var readString = value => {
|
|
134
|
+
return typeof value === "string" && value ? value : void 0;
|
|
135
|
+
};
|
|
136
|
+
/** The reason line, from the response's `error` bag when it carries one. */
|
|
137
|
+
var describeDeployReason = args => {
|
|
138
|
+
const reason = readString(args.error?.message);
|
|
139
|
+
if (!reason) return `No reason was reported on the response; run \`soat list-formation-events --formation-id ${args.formationId ?? "<id>"}\` for the operation history.`;
|
|
140
|
+
return `${readString(args.error?.code) ?? "UNKNOWN"}: ${reason}`;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* A deploy whose reconciliation failed, rendered for stderr.
|
|
144
|
+
*
|
|
145
|
+
* The server answers 2xx here on purpose — the operation ran, and partial
|
|
146
|
+
* failure is modelled on the resource — so the body's `status` is the only
|
|
147
|
+
* signal that anything went wrong. Turning it into a non-zero exit is what
|
|
148
|
+
* stops `&&` chains from reporting a deploy that deployed nothing (#1028).
|
|
149
|
+
*/
|
|
150
|
+
var describeFailedDeploy = args => {
|
|
151
|
+
const {
|
|
152
|
+
commandName,
|
|
153
|
+
data
|
|
154
|
+
} = args;
|
|
155
|
+
if (!DEPLOY_COMMANDS.includes(commandName)) return null;
|
|
156
|
+
if (!isRecord(data) || data.status !== "failed") return null;
|
|
157
|
+
const error = isRecord(data.error) ? data.error : void 0;
|
|
158
|
+
const meta = isRecord(error?.meta) ? error.meta : void 0;
|
|
159
|
+
const logicalId = readString(meta?.logical_id);
|
|
160
|
+
return `${commandName}: the deploy failed${logicalId ? ` at resource '${logicalId}'` : ""} — the formation is 'failed'. ${describeDeployReason({
|
|
161
|
+
error,
|
|
162
|
+
formationId: readString(data.id)
|
|
163
|
+
})}`;
|
|
164
|
+
};
|
|
124
165
|
var formationsWrapper = {
|
|
125
166
|
id: "formations-wrapper",
|
|
126
167
|
commands: FORMATION_COMMANDS,
|
|
168
|
+
failureMessage: describeFailedDeploy,
|
|
127
169
|
helpFlags: [{
|
|
128
170
|
name: "template-path",
|
|
129
171
|
description: "Path to template file (JSON or YAML). Alias: --template-file",
|
|
@@ -332,6 +374,18 @@ var applyWrapperForCommand = args => {
|
|
|
332
374
|
}
|
|
333
375
|
});
|
|
334
376
|
};
|
|
377
|
+
/**
|
|
378
|
+
* What to report for a 2xx payload whose own body says the operation failed,
|
|
379
|
+
* or null when there is nothing wrong with it. Drives the CLI's exit code.
|
|
380
|
+
*/
|
|
381
|
+
var resolveFailureMessage = args => {
|
|
382
|
+
return resolveWrapperForCommand({
|
|
383
|
+
commandName: args.commandName
|
|
384
|
+
})?.failureMessage?.({
|
|
385
|
+
commandName: args.commandName,
|
|
386
|
+
data: args.data
|
|
387
|
+
}) ?? null;
|
|
388
|
+
};
|
|
335
389
|
var getWrapperHelpFlags = commandName => {
|
|
336
390
|
return WRAPPERS.find(w => {
|
|
337
391
|
return w.commands.includes(commandName);
|
|
@@ -745,19 +799,7 @@ var routes = {
|
|
|
745
799
|
"in": "body"
|
|
746
800
|
}, {
|
|
747
801
|
"name": "tool_bindings",
|
|
748
|
-
"description": "Tools to attach, one binding object per tool — the
|
|
749
|
-
"required": false,
|
|
750
|
-
"type": "array",
|
|
751
|
-
"in": "body"
|
|
752
|
-
}, {
|
|
753
|
-
"name": "tool_ids",
|
|
754
|
-
"description": "Deprecated shorthand — each entry becomes a bare `{ \"tool_id\": … }` binding. Use `tool_bindings` instead.",
|
|
755
|
-
"required": false,
|
|
756
|
-
"type": "array",
|
|
757
|
-
"in": "body"
|
|
758
|
-
}, {
|
|
759
|
-
"name": "tools",
|
|
760
|
-
"description": "Deprecated shorthand — each entry becomes a bare `{ \"tool\": … }` binding (an ephemeral definition: no separate Tool resource is created, any `project_id` on an entry is ignored, entries never appear in `GET /tools`, cannot be targeted by `active_tool_ids`/`step_rules`, and cannot be of type `pipeline`). Use `tool_bindings` instead.",
|
|
802
|
+
"description": "Tools to attach, one binding object per tool — the only attachment field. An entry is either a reference (`{ \"tool_id\": … }`) or an inline definition (`{ \"tool\": … }`). See [Tool Bindings](/docs/modules/agents#tool-bindings).",
|
|
761
803
|
"required": false,
|
|
762
804
|
"type": "array",
|
|
763
805
|
"in": "body"
|
|
@@ -909,19 +951,7 @@ var routes = {
|
|
|
909
951
|
"in": "body"
|
|
910
952
|
}, {
|
|
911
953
|
"name": "tool_bindings",
|
|
912
|
-
"description": "Tools attached to the agent — the
|
|
913
|
-
"required": false,
|
|
914
|
-
"type": "array",
|
|
915
|
-
"in": "body"
|
|
916
|
-
}, {
|
|
917
|
-
"name": "tool_ids",
|
|
918
|
-
"description": "Deprecated shorthand — replaces only the reference (`tool_id`) bindings, rewriting them bare. Use `tool_bindings` instead.",
|
|
919
|
-
"required": false,
|
|
920
|
-
"type": "array",
|
|
921
|
-
"in": "body"
|
|
922
|
-
}, {
|
|
923
|
-
"name": "tools",
|
|
924
|
-
"description": "Deprecated shorthand — replaces only the inline (`tool`) bindings (ephemeral definitions: no separate Tool resource is created, any `project_id` on an entry is ignored, entries never appear in `GET /tools`, cannot be targeted by `active_tool_ids`/`step_rules`, and cannot be of type `pipeline`). Set to `null` to clear the inline bindings. Use `tool_bindings` instead.",
|
|
954
|
+
"description": "Tools attached to the agent — the only attachment field. Replaces the whole binding list; set to `null` to clear. See [Tool Bindings](/docs/modules/agents#tool-bindings).",
|
|
925
955
|
"required": false,
|
|
926
956
|
"type": "array",
|
|
927
957
|
"in": "body"
|
|
@@ -1057,19 +1087,7 @@ var routes = {
|
|
|
1057
1087
|
"in": "body"
|
|
1058
1088
|
}, {
|
|
1059
1089
|
"name": "tool_bindings",
|
|
1060
|
-
"description": "Tools attached to the agent — the
|
|
1061
|
-
"required": false,
|
|
1062
|
-
"type": "array",
|
|
1063
|
-
"in": "body"
|
|
1064
|
-
}, {
|
|
1065
|
-
"name": "tool_ids",
|
|
1066
|
-
"description": "Deprecated shorthand — replaces only the reference (`tool_id`) bindings, rewriting them bare. Use `tool_bindings` instead.",
|
|
1067
|
-
"required": false,
|
|
1068
|
-
"type": "array",
|
|
1069
|
-
"in": "body"
|
|
1070
|
-
}, {
|
|
1071
|
-
"name": "tools",
|
|
1072
|
-
"description": "Deprecated shorthand — replaces only the inline (`tool`) bindings (ephemeral definitions: no separate Tool resource is created, any `project_id` on an entry is ignored, entries never appear in `GET /tools`, cannot be targeted by `active_tool_ids`/`step_rules`, and cannot be of type `pipeline`). Set to `null` to clear the inline bindings. Use `tool_bindings` instead.",
|
|
1090
|
+
"description": "Tools attached to the agent — the only attachment field. Replaces the whole binding list; set to `null` to clear. See [Tool Bindings](/docs/modules/agents#tool-bindings).",
|
|
1073
1091
|
"required": false,
|
|
1074
1092
|
"type": "array",
|
|
1075
1093
|
"in": "body"
|
|
@@ -1215,7 +1233,7 @@ var routes = {
|
|
|
1215
1233
|
"in": "body"
|
|
1216
1234
|
}, {
|
|
1217
1235
|
"name": "trace_id",
|
|
1218
|
-
"description": "Optional trace ID to group generations",
|
|
1236
|
+
"description": "Optional trace ID to group generations. Each generation appends its own steps to the trace's steps object, and `step_count` covers them all.",
|
|
1219
1237
|
"required": false,
|
|
1220
1238
|
"type": "string",
|
|
1221
1239
|
"in": "body"
|
|
@@ -1626,7 +1644,7 @@ var routes = {
|
|
|
1626
1644
|
"list-ai-provider-models": {
|
|
1627
1645
|
serviceClass: "AIProviders",
|
|
1628
1646
|
operationId: "listAiProviderModels",
|
|
1629
|
-
description: "Asks the provider which models it can run, using this provider record's own credentials and configuration, and returns provider-native model ids — the same strings `default_model` and an agent's `model` carry. Which models are reachable is a property of the credential, not of the provider type: a Vertex provider sees only the publisher models its Google Cloud project and location serve, and a Bedrock provider only the foundation models enabled in its region. Reading the list is how a caller avoids pinning a model that fails at generation time. Not every provider type can answer. `azure` lists deployments an operator named rather than models, and `ollama` lists whatever was pulled onto that host, so both return `400 MODEL_LISTING_UNSUPPORTED`.",
|
|
1647
|
+
description: "Asks the provider which models it can run, using this provider record's own credentials and configuration, and returns provider-native model ids — the same strings `default_model` and an agent's `model` carry. Which models are reachable is a property of the credential, not of the provider type: a Vertex provider sees only the publisher models its Google Cloud project and location serve, and a Bedrock provider only the foundation models enabled in its region. Reading the list is how a caller avoids pinning a model that fails at generation time. Not every provider type can answer. `azure` lists deployments an operator named rather than models, and `ollama` lists whatever was pulled onto that host, so both return `400 MODEL_LISTING_UNSUPPORTED`. Listing resolves credentials the same way generation does, so a record that can generate can list. The API-key types (`openai`, `groq`, `xai`, `gateway`, `custom`, `anthropic`, `google`) use the record's linked secret and cannot list without one. `bedrock` and `vertex` use the linked secret when there is one — IAM keys or a Bedrock API key, a Google service-account key — and otherwise fall back to the server environment (the AWS default credential chain, Google Application Default Credentials), so a record with no `secret_id` can still list. A Vertex record needs no `config.project` when its secret is a service-account key, since the key file names its own project. A Vertex record in express mode (API key) cannot list at all: express mode is a global, project-less endpoint and the publisher-model catalogue is per-project, so it returns `400 MODEL_LISTING_UNSUPPORTED`.",
|
|
1630
1648
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/ai-providers",
|
|
1631
1649
|
httpMethod: "get",
|
|
1632
1650
|
pathParams: ["ai_provider_id"],
|
|
@@ -1680,7 +1698,7 @@ var routes = {
|
|
|
1680
1698
|
"list-api-keys": {
|
|
1681
1699
|
serviceClass: "APIKeys",
|
|
1682
1700
|
operationId: "listApiKeys",
|
|
1683
|
-
description: "Lists API keys accessible to the caller. - JWT admin: returns all API keys. - JWT regular user: returns only the user's own API keys. - API key: returns only API keys scoped to
|
|
1701
|
+
description: "Lists API keys accessible to the caller. - JWT admin: returns all API keys. - JWT regular user: returns only the user's own API keys. - Project-scoped credential (API key or OAuth token): returns only API keys scoped to that project.",
|
|
1684
1702
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/api-keys",
|
|
1685
1703
|
httpMethod: "get",
|
|
1686
1704
|
pathParams: [],
|
|
@@ -1702,7 +1720,7 @@ var routes = {
|
|
|
1702
1720
|
"create-api-key": {
|
|
1703
1721
|
serviceClass: "APIKeys",
|
|
1704
1722
|
operationId: "createApiKey",
|
|
1705
|
-
description: "Creates a new API key for the authenticated user. - `project_id` is optional. When set, the key is scoped to that single project. When omitted or null, the key is **unscoped** and spans every project its owner can reach. - If `policy_ids` is provided, the key's effective permissions are the intersection of the user's policies and the key's policies. - Otherwise the key inherits the user's permissions (confined to the key's project when scoped).",
|
|
1723
|
+
description: "Creates a new API key for the authenticated user. - `project_id` is optional. When set, the key is scoped to that single project. When omitted or null, the key is **unscoped** and spans every project its owner can reach. - If `policy_ids` is provided, the key's effective permissions are the intersection of the user's policies and the key's policies. - Otherwise the key inherits the user's permissions (confined to the key's project when scoped). - When the request is authenticated with a **project-scoped credential**, the new key is confined to that same project: omitting `project_id` defaults to it, naming a different project returns `403 API_KEY_PROJECT_SCOPE`, and `project_id: null` (an unscoped key) is likewise refused. Minting an unscoped key requires an unscoped credential.",
|
|
1706
1724
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/api-keys",
|
|
1707
1725
|
httpMethod: "post",
|
|
1708
1726
|
pathParams: [],
|
|
@@ -1730,7 +1748,7 @@ var routes = {
|
|
|
1730
1748
|
"get-api-key": {
|
|
1731
1749
|
serviceClass: "APIKeys",
|
|
1732
1750
|
operationId: "getApiKey",
|
|
1733
|
-
description: "Returns details of an API key. Only the owner or an admin can access it.",
|
|
1751
|
+
description: "Returns details of an API key. Only the owner or an admin can access it, and a project-scoped credential can only reach keys in its own project.",
|
|
1734
1752
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/api-keys",
|
|
1735
1753
|
httpMethod: "get",
|
|
1736
1754
|
pathParams: ["api_key_id"],
|
|
@@ -1746,7 +1764,7 @@ var routes = {
|
|
|
1746
1764
|
"update-api-key": {
|
|
1747
1765
|
serviceClass: "APIKeys",
|
|
1748
1766
|
operationId: "updateApiKey",
|
|
1749
|
-
description: "Updates an API key's name, project scope, or policies. The project scope can be changed to another project, set (scoping a previously unscoped key), or cleared with null (unscoping the key). Only the owner or an admin can update it.",
|
|
1767
|
+
description: "Updates an API key's name, project scope, or policies. The project scope can be changed to another project, set (scoping a previously unscoped key), or cleared with null (unscoping the key). Only the owner or an admin can update it. A project-scoped credential can only update keys in its own project, and cannot move a key to another project or unscope it.",
|
|
1750
1768
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/api-keys",
|
|
1751
1769
|
httpMethod: "put",
|
|
1752
1770
|
pathParams: ["api_key_id"],
|
|
@@ -1780,7 +1798,7 @@ var routes = {
|
|
|
1780
1798
|
"delete-api-key": {
|
|
1781
1799
|
serviceClass: "APIKeys",
|
|
1782
1800
|
operationId: "deleteApiKey",
|
|
1783
|
-
description: "Deletes an API key. Only the owner or an admin can delete it.",
|
|
1801
|
+
description: "Deletes an API key. Only the owner or an admin can delete it, and a project-scoped credential can only delete keys in its own project.",
|
|
1784
1802
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/api-keys",
|
|
1785
1803
|
httpMethod: "delete",
|
|
1786
1804
|
pathParams: ["api_key_id"],
|
|
@@ -2126,7 +2144,7 @@ var routes = {
|
|
|
2126
2144
|
"type": "string",
|
|
2127
2145
|
"in": "body"
|
|
2128
2146
|
}, {
|
|
2129
|
-
"name": "
|
|
2147
|
+
"name": "instructions",
|
|
2130
2148
|
"description": "Optional system message applied to all completions on this chat",
|
|
2131
2149
|
"required": false,
|
|
2132
2150
|
"type": "string",
|
|
@@ -2171,57 +2189,35 @@ var routes = {
|
|
|
2171
2189
|
"in": "path"
|
|
2172
2190
|
}]
|
|
2173
2191
|
},
|
|
2174
|
-
"create-chat-completion
|
|
2192
|
+
"create-chat-completion": {
|
|
2175
2193
|
serviceClass: "Chats",
|
|
2176
|
-
operationId: "
|
|
2177
|
-
description: "
|
|
2194
|
+
operationId: "createChatCompletion",
|
|
2195
|
+
description: "OpenAI Chat Completions-compatible endpoint. Mirrors OpenAI's `POST /v1/chat/completions` path so an OpenAI SDK can target it by base URL alone. Names exactly one target. With `ai_provider_id` the completion is stateless: the provider's secret is decrypted and the appropriate Vercel AI SDK provider is called, with no server-side model fallback. With `chat_id` the stored chat supplies the provider (or the project's `default_model_route_id`), model and instructions. System content travels only in `instructions` — a `role: \"system\"` entry in `messages` is refused with `400 SYSTEM_MESSAGE_NOT_ALLOWED`. With `chat_id`, a request `instructions` replaces the chat's stored one for this call only; the stored value applies when the request carries none, and the two are never merged. Messages may use `document_id` instead of `content` with either target. Chats hold no message history — send the full `messages` array every time.",
|
|
2178
2196
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/chats",
|
|
2179
2197
|
httpMethod: "post",
|
|
2180
|
-
pathParams: [
|
|
2198
|
+
pathParams: [],
|
|
2181
2199
|
queryParams: [],
|
|
2182
2200
|
flags: [{
|
|
2183
|
-
"name": "
|
|
2184
|
-
"description": "",
|
|
2185
|
-
"required":
|
|
2201
|
+
"name": "ai_provider_id",
|
|
2202
|
+
"description": "Public ID of the AI provider to run the completion against. Mutually exclusive with `chat_id`; exactly one of the two is required.\n",
|
|
2203
|
+
"required": false,
|
|
2186
2204
|
"type": "string",
|
|
2187
|
-
"in": "path"
|
|
2188
|
-
}, {
|
|
2189
|
-
"name": "messages",
|
|
2190
|
-
"description": "",
|
|
2191
|
-
"required": true,
|
|
2192
|
-
"type": "array",
|
|
2193
2205
|
"in": "body"
|
|
2194
2206
|
}, {
|
|
2195
|
-
"name": "
|
|
2196
|
-
"description": "
|
|
2207
|
+
"name": "chat_id",
|
|
2208
|
+
"description": "Public ID of a stored chat supplying the provider, model and instructions. Mutually exclusive with `ai_provider_id`; exactly one of the two is required.\n",
|
|
2197
2209
|
"required": false,
|
|
2198
2210
|
"type": "string",
|
|
2199
2211
|
"in": "body"
|
|
2200
2212
|
}, {
|
|
2201
|
-
"name": "
|
|
2202
|
-
"description": "
|
|
2213
|
+
"name": "model",
|
|
2214
|
+
"description": "Model identifier. Overrides the provider's `default_model`, or the chat's `model`, when specified.\n",
|
|
2203
2215
|
"required": false,
|
|
2204
|
-
"type": "boolean",
|
|
2205
|
-
"in": "body"
|
|
2206
|
-
}]
|
|
2207
|
-
},
|
|
2208
|
-
"create-chat-completion": {
|
|
2209
|
-
serviceClass: "Chats",
|
|
2210
|
-
operationId: "createChatCompletion",
|
|
2211
|
-
description: "OpenAI Chat Completions-compatible endpoint. Mirrors OpenAI's `POST /v1/chat/completions` path so an OpenAI SDK can target it by base URL alone. Resolves the AI provider from `ai_provider_id`, decrypts its secret, and calls the appropriate Vercel AI SDK provider. `ai_provider_id` is required — there is no server-side model fallback.",
|
|
2212
|
-
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/chats",
|
|
2213
|
-
httpMethod: "post",
|
|
2214
|
-
pathParams: [],
|
|
2215
|
-
queryParams: [],
|
|
2216
|
-
flags: [{
|
|
2217
|
-
"name": "ai_provider_id",
|
|
2218
|
-
"description": "Public ID of the AI provider to use.",
|
|
2219
|
-
"required": true,
|
|
2220
2216
|
"type": "string",
|
|
2221
2217
|
"in": "body"
|
|
2222
2218
|
}, {
|
|
2223
|
-
"name": "
|
|
2224
|
-
"description": "
|
|
2219
|
+
"name": "instructions",
|
|
2220
|
+
"description": "System instructions for this call. Sent to the provider as its `instructions` argument rather than as a message, which is the only place the AI SDK accepts system content (`allowSystemInMessages` defaults to false). This field is the only channel — a `role: \"system\"` entry in `messages` is refused with `400 SYSTEM_MESSAGE_NOT_ALLOWED`. With `chat_id`, this replaces the chat's stored `instructions` for this call only; the stored value applies when the request carries none, and the two are never merged.\n",
|
|
2225
2221
|
"required": false,
|
|
2226
2222
|
"type": "string",
|
|
2227
2223
|
"in": "body"
|
|
@@ -3129,6 +3125,40 @@ var routes = {
|
|
|
3129
3125
|
"in": "body"
|
|
3130
3126
|
}]
|
|
3131
3127
|
},
|
|
3128
|
+
"create-dataset-item-from-generation": {
|
|
3129
|
+
serviceClass: "Evaluations",
|
|
3130
|
+
operationId: "createDatasetItemFromGeneration",
|
|
3131
|
+
description: "Promotes a real, completed generation into a test case: its input messages become the item's `input`, and its own answer becomes `expected_output` unless you supply one. Use it to build an evaluation set out of production traffic rather than hand-authoring fixtures. The item is a **copy**, not a view. It keeps working after the source generation's content is purged, and `source_generation_id` goes null if that generation is deleted — a purge can never quietly stop a suite from being runnable. Requires both `evaluations:CreateDataset` and `generations:GetGeneration`: the call copies content out of a generation, so a principal that may not read that generation may not curate it either. Only a **completed** generation can be promoted (`409 GENERATION_NOT_COMPLETED`), and only while its content is still available: an agent or project running with `trace_content_mode: none` never stored the input, and a purged or expired generation no longer has it (`409 GENERATION_CONTENT_UNAVAILABLE`). Generations that predate input recording answer the same way.",
|
|
3132
|
+
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/evaluations",
|
|
3133
|
+
httpMethod: "post",
|
|
3134
|
+
pathParams: ["dataset_id"],
|
|
3135
|
+
queryParams: [],
|
|
3136
|
+
flags: [{
|
|
3137
|
+
"name": "dataset_id",
|
|
3138
|
+
"description": "Dataset ID",
|
|
3139
|
+
"required": true,
|
|
3140
|
+
"type": "string",
|
|
3141
|
+
"in": "path"
|
|
3142
|
+
}, {
|
|
3143
|
+
"name": "generation_id",
|
|
3144
|
+
"description": "The completed generation to promote. Must belong to the same project as the dataset.",
|
|
3145
|
+
"required": true,
|
|
3146
|
+
"type": "string",
|
|
3147
|
+
"in": "body"
|
|
3148
|
+
}, {
|
|
3149
|
+
"name": "expected_output",
|
|
3150
|
+
"description": "Reference answer. Omit to use the generation's own answer; pass `null` to store the item with no reference answer.",
|
|
3151
|
+
"required": false,
|
|
3152
|
+
"type": "string",
|
|
3153
|
+
"in": "body"
|
|
3154
|
+
}, {
|
|
3155
|
+
"name": "metadata",
|
|
3156
|
+
"description": "Free-form tags, opaque to the platform",
|
|
3157
|
+
"required": false,
|
|
3158
|
+
"type": "object",
|
|
3159
|
+
"in": "body"
|
|
3160
|
+
}]
|
|
3161
|
+
},
|
|
3132
3162
|
"update-dataset-item": {
|
|
3133
3163
|
serviceClass: "Evaluations",
|
|
3134
3164
|
operationId: "updateDatasetItem",
|
|
@@ -4020,7 +4050,7 @@ var routes = {
|
|
|
4020
4050
|
"create-formation": {
|
|
4021
4051
|
serviceClass: "Formations",
|
|
4022
4052
|
operationId: "createFormation",
|
|
4023
|
-
description: "Validates the template, creates the formation record, then provisions all declared resources in dependency order.",
|
|
4053
|
+
description: "Validates the template, creates the formation record, then provisions all declared resources in dependency order. A **template-shape** error is refused with `400`. A **deploy** failure is not: the operation ran, so the formation is returned with `201` and `status: \"failed\"`, and `error` explains why (the resources created before the failure are rolled back). Read `status` — a `2xx` here means the deploy was attempted, not that it worked. The `soat` CLI exits non-zero on that body so `create-formation && …` does not lie.",
|
|
4024
4054
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/formations",
|
|
4025
4055
|
httpMethod: "post",
|
|
4026
4056
|
pathParams: [],
|
|
@@ -4076,7 +4106,7 @@ var routes = {
|
|
|
4076
4106
|
"update-formation": {
|
|
4077
4107
|
serviceClass: "Formations",
|
|
4078
4108
|
operationId: "updateFormation",
|
|
4079
|
-
description: "Applies a new template to the formation. Resources are created, updated, or deleted to reconcile the current state with the desired state.",
|
|
4109
|
+
description: "Applies a new template to the formation. Resources are created, updated, or deleted to reconcile the current state with the desired state. A **template-shape** error is refused with `400`. A **deploy** failure is not: the operation ran, so the formation is returned with `200` and `status: \"failed\"`, and `error` explains why. Read `status` — a `2xx` here means the deploy was attempted, not that it worked. The `soat` CLI exits non-zero on that body so `update-formation && …` does not lie.",
|
|
4080
4110
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/formations",
|
|
4081
4111
|
httpMethod: "put",
|
|
4082
4112
|
pathParams: ["formation_id"],
|
|
@@ -4110,7 +4140,7 @@ var routes = {
|
|
|
4110
4140
|
"delete-formation": {
|
|
4111
4141
|
serviceClass: "Formations",
|
|
4112
4142
|
operationId: "deleteFormation",
|
|
4113
|
-
description: "Deletes the formation stack and all its managed resources in reverse dependency order.",
|
|
4143
|
+
description: "Deletes the formation stack and all its managed resources in reverse dependency order. A resource the platform refuses to delete on its own — most often an agent that has generation or trace history — fails the teardown with `409 FORMATION_DELETE_FAILED`, naming every blocking resource in `error.meta.failures`. Resolve the blockers (for an agent, `DELETE /api/v1/agents/{agent_id}?force=true` also removes its generations and traces, and `deletion_policy: retain` exempts it from teardown entirely) and delete the formation again. A refusal the platform can foresee is found by a pre-flight, before the first delete: nothing is removed, and the formation stays `active` and intact for the retry. An unforeseeable error surfaces mid-teardown instead, where resources deleted before the blocker stay deleted and the formation is left in `delete_failed`. The error message states which happened.",
|
|
4114
4144
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/formations",
|
|
4115
4145
|
httpMethod: "delete",
|
|
4116
4146
|
pathParams: ["formation_id"],
|
|
@@ -4251,6 +4281,22 @@ var routes = {
|
|
|
4251
4281
|
"in": "path"
|
|
4252
4282
|
}]
|
|
4253
4283
|
},
|
|
4284
|
+
"get-generation-transcript": {
|
|
4285
|
+
serviceClass: "Generations",
|
|
4286
|
+
operationId: "getGenerationTranscript",
|
|
4287
|
+
description: "Returns one generation's turn read back as an ordered sequence of steps: what it was asked, each model step with its tool calls and results, and how it ended. The transcript is assembled at read time from the generation record and the trace's steps object; nothing is stored, so it cannot outlive the content it projects. Requires `traces:GetTrace` in addition to `generations:GetGeneration`, because the response merges content from both resources. A generation whose content is unavailable — never written under zero-retention, or cleared by a purge — returns `200` with the skeleton rather than an error: `input` and `output` are null, `steps` is empty, and the `content_redacted_*` fields say which happened. `content_redacted_by_principal_id` is `zero_retention` when the content was never stored, and the purging principal's ID when it was erased later. A generation that is still running returns the same shape with an empty `steps`; `status` disambiguates the two.",
|
|
4288
|
+
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/generations",
|
|
4289
|
+
httpMethod: "get",
|
|
4290
|
+
pathParams: ["generation_id"],
|
|
4291
|
+
queryParams: [],
|
|
4292
|
+
flags: [{
|
|
4293
|
+
"name": "generation_id",
|
|
4294
|
+
"description": "Public ID of the generation",
|
|
4295
|
+
"required": true,
|
|
4296
|
+
"type": "string",
|
|
4297
|
+
"in": "path"
|
|
4298
|
+
}]
|
|
4299
|
+
},
|
|
4254
4300
|
"list-guardrails": {
|
|
4255
4301
|
serviceClass: "Guardrails",
|
|
4256
4302
|
operationId: "listGuardrails",
|
|
@@ -4773,7 +4819,7 @@ var routes = {
|
|
|
4773
4819
|
"in": "body"
|
|
4774
4820
|
}, {
|
|
4775
4821
|
"name": "min_score",
|
|
4776
|
-
"description": "Minimum
|
|
4822
|
+
"description": "Minimum `score` a result must reach to be returned. Filters on the implementation-defined `score`, not on `similarity_score`, so the cutoff follows the ranking. Only applies when `query` is provided. Because the scale behind `score` is not part of the contract, treat a tuned value as tied to the deployment rather than portable.",
|
|
4777
4823
|
"required": false,
|
|
4778
4824
|
"type": "number",
|
|
4779
4825
|
"in": "body"
|
|
@@ -4950,7 +4996,7 @@ var routes = {
|
|
|
4950
4996
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/memoryEntries",
|
|
4951
4997
|
httpMethod: "get",
|
|
4952
4998
|
pathParams: [],
|
|
4953
|
-
queryParams: ["memory_id", "limit", "offset"],
|
|
4999
|
+
queryParams: ["memory_id", "limit", "offset", "include_invalidated"],
|
|
4954
5000
|
flags: [{
|
|
4955
5001
|
"name": "memory_id",
|
|
4956
5002
|
"description": "Memory container to list entries from (mem_...)",
|
|
@@ -4969,6 +5015,12 @@ var routes = {
|
|
|
4969
5015
|
"required": false,
|
|
4970
5016
|
"type": "integer",
|
|
4971
5017
|
"in": "query"
|
|
5018
|
+
}, {
|
|
5019
|
+
"name": "include_invalidated",
|
|
5020
|
+
"description": "Include invalidated (superseded) entries. They are excluded by default; set this to audit the supersede history.",
|
|
5021
|
+
"required": false,
|
|
5022
|
+
"type": "boolean",
|
|
5023
|
+
"in": "query"
|
|
4972
5024
|
}]
|
|
4973
5025
|
},
|
|
4974
5026
|
"create-memory-entry": {
|
|
@@ -5564,7 +5616,7 @@ var routes = {
|
|
|
5564
5616
|
"start-orchestration-run": {
|
|
5565
5617
|
serviceClass: "Orchestrations",
|
|
5566
5618
|
operationId: "startOrchestrationRun",
|
|
5567
|
-
description: "Creates a new run for the orchestration named by orchestration_id. By default the run executes durably in the background: the response returns immediately with status \"queued\" (a worker then claims it and moves it to \"running\") and progress is observed via get-orchestration-run or run lifecycle webhook events (orchestration_runs.started/awaiting_input/succeeded/failed). Delay and poll waits park the run as \"sleeping\" and are woken by a background scheduler, surviving restarts. Pass wait=true to block until the run reaches a terminal or awaiting_input state
|
|
5619
|
+
description: "Creates a new run for the orchestration named by orchestration_id. By default the run executes durably in the background: the response returns immediately with status \"queued\" (a worker then claims it and moves it to \"running\") and progress is observed via get-orchestration-run or run lifecycle webhook events (orchestration_runs.started/awaiting_input/succeeded/failed). Delay and poll waits park the run as \"sleeping\" and are woken by a background scheduler, surviving restarts. Pass wait=true to block until the run reaches a terminal or awaiting_input state.",
|
|
5568
5620
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/orchestrations",
|
|
5569
5621
|
httpMethod: "post",
|
|
5570
5622
|
pathParams: [],
|
|
@@ -6499,6 +6551,80 @@ var routes = {
|
|
|
6499
6551
|
"in": "body"
|
|
6500
6552
|
}]
|
|
6501
6553
|
},
|
|
6554
|
+
"fork-session": {
|
|
6555
|
+
serviceClass: "Sessions",
|
|
6556
|
+
operationId: "forkSession",
|
|
6557
|
+
description: "Branches a new session from a point in this session's history: same context, different continuation. The fork gets its own conversation whose messages **reference the same documents** as the parent rather than copying them, so there is one stored copy of the content and a retention purge erases it from both. Recorded tool results ride along on those messages and are **replayed** as model input on the fork's next turn — forking never re-invokes a tool, so exploring a \"what if\" cannot send an email or charge a card a second time. The consequence to accept is that a forked turn sees the tool data as it was, not as it is now. The fork is created **inert**: `auto_generate` is false and no generation is triggered. Drive it with the normal message and generate endpoints. The fork has no actor — attach one only if the branch is meant to be driven by the same end user, since `single_session_per_actor` agents allow one open session per actor.",
|
|
6558
|
+
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/sessions",
|
|
6559
|
+
httpMethod: "post",
|
|
6560
|
+
pathParams: ["session_id"],
|
|
6561
|
+
queryParams: [],
|
|
6562
|
+
flags: [{
|
|
6563
|
+
"name": "session_id",
|
|
6564
|
+
"description": "Session public ID",
|
|
6565
|
+
"required": true,
|
|
6566
|
+
"type": "string",
|
|
6567
|
+
"in": "path"
|
|
6568
|
+
}, {
|
|
6569
|
+
"name": "fork_at_position",
|
|
6570
|
+
"description": "The parent conversation `position` to branch after. Messages at positions 0..N are carried into the fork. Omit it to branch at the tip (the whole history).\n",
|
|
6571
|
+
"required": false,
|
|
6572
|
+
"type": "integer",
|
|
6573
|
+
"in": "body"
|
|
6574
|
+
}, {
|
|
6575
|
+
"name": "agent_id",
|
|
6576
|
+
"description": "Agent the fork runs against. Defaults to the parent session's agent; overriding it is the point of forking — same context, a different agent or agent version. Must belong to the same project as the session being forked.\n",
|
|
6577
|
+
"required": false,
|
|
6578
|
+
"type": "string",
|
|
6579
|
+
"in": "body"
|
|
6580
|
+
}, {
|
|
6581
|
+
"name": "name",
|
|
6582
|
+
"description": "Optional name for the forked session",
|
|
6583
|
+
"required": false,
|
|
6584
|
+
"type": "string",
|
|
6585
|
+
"in": "body"
|
|
6586
|
+
}, {
|
|
6587
|
+
"name": "tags",
|
|
6588
|
+
"description": "Optional tags for the forked session",
|
|
6589
|
+
"required": false,
|
|
6590
|
+
"type": "object",
|
|
6591
|
+
"in": "body"
|
|
6592
|
+
}, {
|
|
6593
|
+
"name": "tool_context",
|
|
6594
|
+
"description": "Overrides the parent's `tool_context` on the fork. Omit it and the fork inherits the parent's, so the branch is faithful to the run it came from.\n",
|
|
6595
|
+
"required": false,
|
|
6596
|
+
"type": "object",
|
|
6597
|
+
"in": "body"
|
|
6598
|
+
}]
|
|
6599
|
+
},
|
|
6600
|
+
"list-session-forks": {
|
|
6601
|
+
serviceClass: "Sessions",
|
|
6602
|
+
operationId: "listSessionForks",
|
|
6603
|
+
description: "Returns the sessions forked directly from this one. One level of lineage: a fork of a fork is listed under its own parent.",
|
|
6604
|
+
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/sessions",
|
|
6605
|
+
httpMethod: "get",
|
|
6606
|
+
pathParams: ["session_id"],
|
|
6607
|
+
queryParams: ["limit", "offset"],
|
|
6608
|
+
flags: [{
|
|
6609
|
+
"name": "session_id",
|
|
6610
|
+
"description": "Session public ID",
|
|
6611
|
+
"required": true,
|
|
6612
|
+
"type": "string",
|
|
6613
|
+
"in": "path"
|
|
6614
|
+
}, {
|
|
6615
|
+
"name": "limit",
|
|
6616
|
+
"description": "",
|
|
6617
|
+
"required": false,
|
|
6618
|
+
"type": "integer",
|
|
6619
|
+
"in": "query"
|
|
6620
|
+
}, {
|
|
6621
|
+
"name": "offset",
|
|
6622
|
+
"description": "",
|
|
6623
|
+
"required": false,
|
|
6624
|
+
"type": "integer",
|
|
6625
|
+
"in": "query"
|
|
6626
|
+
}]
|
|
6627
|
+
},
|
|
6502
6628
|
"get-session-tags": {
|
|
6503
6629
|
serviceClass: "Sessions",
|
|
6504
6630
|
operationId: "getSessionTags",
|
|
@@ -7444,11 +7570,11 @@ var routes = {
|
|
|
7444
7570
|
"list-usage-meters": {
|
|
7445
7571
|
serviceClass: "Usage",
|
|
7446
7572
|
operationId: "listUsageMeters",
|
|
7447
|
-
description: "Returns the raw usage-meter rows the caller can access, most recent first, optionally filtered by agent, generation, trace, actor, or
|
|
7573
|
+
description: "Returns the raw usage-meter rows the caller can access, most recent first, optionally filtered by agent, generation, trace, actor, session, or `source`. Each row is the per-generation token usage as reported by the provider, for audit and reconciliation.",
|
|
7448
7574
|
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/usage",
|
|
7449
7575
|
httpMethod: "get",
|
|
7450
7576
|
pathParams: [],
|
|
7451
|
-
queryParams: ["agent_id", "generation_id", "trace_id", "actor_id", "session_id", "trigger_id", "action_id", "meter_type", "limit", "offset"],
|
|
7577
|
+
queryParams: ["agent_id", "generation_id", "trace_id", "actor_id", "session_id", "trigger_id", "action_id", "meter_type", "source", "limit", "offset"],
|
|
7452
7578
|
flags: [{
|
|
7453
7579
|
"name": "agent_id",
|
|
7454
7580
|
"description": "Filter by agent public ID",
|
|
@@ -7497,6 +7623,12 @@ var routes = {
|
|
|
7497
7623
|
"required": false,
|
|
7498
7624
|
"type": "string",
|
|
7499
7625
|
"in": "query"
|
|
7626
|
+
}, {
|
|
7627
|
+
"name": "source",
|
|
7628
|
+
"description": "Filter by what the spend was incurred for. `eval` is an eval run's item generations and `eval_judge` an `llm_judge` scorer's own completion, so verification spend is `source` in (`eval`, `eval_judge`). Ordinary agent traffic carries no source and is matched by neither.\n",
|
|
7629
|
+
"required": false,
|
|
7630
|
+
"type": "string",
|
|
7631
|
+
"in": "query"
|
|
7500
7632
|
}, {
|
|
7501
7633
|
"name": "limit",
|
|
7502
7634
|
"description": "",
|
|
@@ -7527,7 +7659,7 @@ var routes = {
|
|
|
7527
7659
|
"in": "query"
|
|
7528
7660
|
}, {
|
|
7529
7661
|
"name": "group_by",
|
|
7530
|
-
"description": "Dimension to bucket by. `day` buckets on the event's UTC calendar day; the others bucket on the matching column.\n",
|
|
7662
|
+
"description": "Dimension to bucket by. `day` buckets on the event's UTC calendar day; the others bucket on the matching column. `source` buckets by what the spend was incurred for (`eval`, `eval_judge`), which is how verification spend is priced apart from the traffic serving real users; unlabelled traffic collapses into the single `null` bucket.\n",
|
|
7531
7663
|
"required": true,
|
|
7532
7664
|
"type": "string",
|
|
7533
7665
|
"in": "query"
|
|
@@ -8037,6 +8169,22 @@ var routes = {
|
|
|
8037
8169
|
"in": "path"
|
|
8038
8170
|
}]
|
|
8039
8171
|
},
|
|
8172
|
+
"redeliver-webhook-delivery": {
|
|
8173
|
+
serviceClass: "Webhooks",
|
|
8174
|
+
operationId: "redeliverWebhookDelivery",
|
|
8175
|
+
description: "Queues the stored payload of an existing delivery to be sent again. A new delivery record is created rather than the original being reset, so the original attempt stays in the history. The send happens in the background: poll the returned delivery to observe its outcome.",
|
|
8176
|
+
moduleDocsUrl: "https://soat.ttoss.dev/docs/modules/webhooks",
|
|
8177
|
+
httpMethod: "post",
|
|
8178
|
+
pathParams: ["delivery_id"],
|
|
8179
|
+
queryParams: [],
|
|
8180
|
+
flags: [{
|
|
8181
|
+
"name": "delivery_id",
|
|
8182
|
+
"description": "Delivery to send again (wh_deliv_...)",
|
|
8183
|
+
"required": true,
|
|
8184
|
+
"type": "string",
|
|
8185
|
+
"in": "path"
|
|
8186
|
+
}]
|
|
8187
|
+
},
|
|
8040
8188
|
"get-webhook-secret": {
|
|
8041
8189
|
serviceClass: "Webhooks",
|
|
8042
8190
|
operationId: "getWebhookSecret",
|
|
@@ -8314,7 +8462,12 @@ var routes = {
|
|
|
8314
8462
|
};
|
|
8315
8463
|
|
|
8316
8464
|
//#endregion
|
|
8317
|
-
//#region src/
|
|
8465
|
+
//#region src/naming.ts
|
|
8466
|
+
/**
|
|
8467
|
+
* Flag-name spellings. The CLI accepts kebab-case (the documented convention),
|
|
8468
|
+
* snake_case (the wire spelling, so a body field can be typed exactly as the
|
|
8469
|
+
* spec names it), and camelCase alike; matching happens on the canonical form.
|
|
8470
|
+
*/
|
|
8318
8471
|
/**
|
|
8319
8472
|
* Normalize kebab-case, snake_case, or camelCase to camelCase for param matching.
|
|
8320
8473
|
* e.g. agent-id → agentId, actor_id → actorId, agentId → agentId
|
|
@@ -8338,6 +8491,143 @@ var kebabToSnake = s => {
|
|
|
8338
8491
|
var toKebab = s => {
|
|
8339
8492
|
return s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
8340
8493
|
};
|
|
8494
|
+
|
|
8495
|
+
//#endregion
|
|
8496
|
+
//#region src/validateFlags.ts
|
|
8497
|
+
/** Accepted on any command: Commander globals plus the generic id alias. */
|
|
8498
|
+
var ALWAYS_ALLOWED = /* @__PURE__ */new Set(["profile", "id"]);
|
|
8499
|
+
/**
|
|
8500
|
+
* Every flag name this command accepts, canonicalized. Wrapper-added flags are
|
|
8501
|
+
* absent on purpose — a wrapper deletes the flags it consumed before this runs,
|
|
8502
|
+
* so anything left over really is unrecognized.
|
|
8503
|
+
*/
|
|
8504
|
+
var knownFlagsFor = route => {
|
|
8505
|
+
return new Set([...route.flags.map(f => {
|
|
8506
|
+
return f.name;
|
|
8507
|
+
}), ...route.pathParams, ...route.queryParams].map(toCanonical));
|
|
8508
|
+
};
|
|
8509
|
+
/**
|
|
8510
|
+
* Offer the closest known flag, so a typo is one line away from its fix rather
|
|
8511
|
+
* than a trip to `--help`. Prefix matching in either direction covers the real
|
|
8512
|
+
* cases (`limitt`/`limit`, `agent`/`agent_id`) without a full edit-distance pass.
|
|
8513
|
+
*/
|
|
8514
|
+
var suggestionFor = args => {
|
|
8515
|
+
const target = toCanonical(args.flag).toLowerCase();
|
|
8516
|
+
const near = [...args.known].find(known => {
|
|
8517
|
+
const candidate = known.toLowerCase();
|
|
8518
|
+
return candidate.startsWith(target) || target.startsWith(candidate);
|
|
8519
|
+
});
|
|
8520
|
+
return near ? ` Did you mean --${toKebab(near)}?` : "";
|
|
8521
|
+
};
|
|
8522
|
+
/**
|
|
8523
|
+
* Reject an unrecognized flag **only where the server cannot catch it** — a flag
|
|
8524
|
+
* the CLI would otherwise append to the query string.
|
|
8525
|
+
*
|
|
8526
|
+
* An undeclared query param never reaches a check: the server ignores what it
|
|
8527
|
+
* does not know, so `list-agents --limitt 1` returned every row instead of one.
|
|
8528
|
+
* The filter failed **open**, with exit 0 and no warning, and the caller acted on
|
|
8529
|
+
* a superset it never asked for. The name does not survive the request, so only
|
|
8530
|
+
* the client can catch it.
|
|
8531
|
+
*
|
|
8532
|
+
* An unrecognized flag on a **write** is deliberately still forwarded. The server
|
|
8533
|
+
* already answers `400 VALIDATION_FAILED` naming the field (`strictFields`), and
|
|
8534
|
+
* that check is the authority on what a body may contain — rejecting locally
|
|
8535
|
+
* would front-run it, hide the real error, and make an older CLI refuse a field a
|
|
8536
|
+
* newer server accepts. It would also make the behavior untestable through the
|
|
8537
|
+
* CLI: `tests/smoke-tests.sh` asserts precisely that the server rejects
|
|
8538
|
+
* `update-agent --reasoning` with a 400, which a client-side refusal turns into a
|
|
8539
|
+
* usage error the assertion cannot read.
|
|
8540
|
+
*
|
|
8541
|
+
* Returns the error lines to print; empty when every flag is recognized.
|
|
8542
|
+
*/
|
|
8543
|
+
var findUnknownFlags = args => {
|
|
8544
|
+
if (args.route.httpMethod !== "get") return [];
|
|
8545
|
+
const known = knownFlagsFor(args.route);
|
|
8546
|
+
const unknown = args.flagKeys.filter(flagKey => {
|
|
8547
|
+
if (ALWAYS_ALLOWED.has(flagKey)) return false;
|
|
8548
|
+
return !known.has(toCanonical(flagKey));
|
|
8549
|
+
});
|
|
8550
|
+
if (unknown.length === 0) return [];
|
|
8551
|
+
return [...unknown.map(flag => {
|
|
8552
|
+
return `Unknown flag --${flag} for '${args.commandName}'.${suggestionFor({
|
|
8553
|
+
flag,
|
|
8554
|
+
known
|
|
8555
|
+
})}`;
|
|
8556
|
+
}), `Run "soat ${args.commandName} --help" to see the flags this command accepts.`];
|
|
8557
|
+
};
|
|
8558
|
+
|
|
8559
|
+
//#endregion
|
|
8560
|
+
//#region src/webhookSignature.ts
|
|
8561
|
+
var digestMatches = args => {
|
|
8562
|
+
const expectedBuffer = Buffer.from(args.expected, "utf8");
|
|
8563
|
+
const actualBuffer = Buffer.from(args.actual, "utf8");
|
|
8564
|
+
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
8565
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
8566
|
+
};
|
|
8567
|
+
/** Deprecated scheme: `sha256=<hex>` over the bare body. */
|
|
8568
|
+
var verifyLegacySignature = args => {
|
|
8569
|
+
const expected = createHmac("sha256", args.secret).update(args.payload).digest("hex");
|
|
8570
|
+
return digestMatches({
|
|
8571
|
+
expected,
|
|
8572
|
+
actual: args.header
|
|
8573
|
+
});
|
|
8574
|
+
};
|
|
8575
|
+
/**
|
|
8576
|
+
* Timestamped scheme: `t=<unix>,v1=<hex>` over `<t>.<body>`.
|
|
8577
|
+
*
|
|
8578
|
+
* The timestamp must be present, but its age is deliberately not enforced here:
|
|
8579
|
+
* this is a local debugging listener, and rejecting a delivery over clock skew
|
|
8580
|
+
* would read as a signing bug. A real subscriber should enforce a tolerance
|
|
8581
|
+
* window — the webhooks module docs show one.
|
|
8582
|
+
*/
|
|
8583
|
+
var verifyTimestampedSignature = args => {
|
|
8584
|
+
const elements = new Map(args.header.split(",").map(part => {
|
|
8585
|
+
const separator = part.indexOf("=");
|
|
8586
|
+
return [part.slice(0, separator).trim(), part.slice(separator + 1)];
|
|
8587
|
+
}));
|
|
8588
|
+
const timestamp = elements.get("t");
|
|
8589
|
+
const digest = elements.get("v1");
|
|
8590
|
+
if (!timestamp || !digest) return false;
|
|
8591
|
+
const expected = createHmac("sha256", args.secret).update(`${timestamp}.${args.payload}`).digest("hex");
|
|
8592
|
+
return digestMatches({
|
|
8593
|
+
expected,
|
|
8594
|
+
actual: digest
|
|
8595
|
+
});
|
|
8596
|
+
};
|
|
8597
|
+
var headerValue = value => {
|
|
8598
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
8599
|
+
return value ?? "";
|
|
8600
|
+
};
|
|
8601
|
+
/**
|
|
8602
|
+
* Picks the scheme a delivery used and verifies it, when a secret is supplied.
|
|
8603
|
+
*/
|
|
8604
|
+
var inspectDeliverySignature = args => {
|
|
8605
|
+
const timestamped = headerValue(args.headers["x-soat-signature-v2"]);
|
|
8606
|
+
const legacy = headerValue(args.headers["x-soat-signature"]);
|
|
8607
|
+
const scheme = timestamped ? "v2" : "v1";
|
|
8608
|
+
const signature = timestamped || legacy;
|
|
8609
|
+
if (!args.secret) return {
|
|
8610
|
+
signature,
|
|
8611
|
+
scheme,
|
|
8612
|
+
valid: null
|
|
8613
|
+
};
|
|
8614
|
+
return {
|
|
8615
|
+
signature,
|
|
8616
|
+
scheme,
|
|
8617
|
+
valid: scheme === "v2" ? verifyTimestampedSignature({
|
|
8618
|
+
secret: args.secret,
|
|
8619
|
+
payload: args.payload,
|
|
8620
|
+
header: timestamped
|
|
8621
|
+
}) : verifyLegacySignature({
|
|
8622
|
+
secret: args.secret,
|
|
8623
|
+
payload: args.payload,
|
|
8624
|
+
header: legacy
|
|
8625
|
+
})
|
|
8626
|
+
};
|
|
8627
|
+
};
|
|
8628
|
+
|
|
8629
|
+
//#endregion
|
|
8630
|
+
//#region src/index.ts
|
|
8341
8631
|
/**
|
|
8342
8632
|
* Renders a command's payload for stdout.
|
|
8343
8633
|
*
|
|
@@ -8400,14 +8690,7 @@ var matchesFilter = (eventType, filter) => {
|
|
|
8400
8690
|
return eventType === pattern;
|
|
8401
8691
|
});
|
|
8402
8692
|
};
|
|
8403
|
-
|
|
8404
|
-
const expected = createHmac("sha256", secret).update(payload).digest("hex");
|
|
8405
|
-
const expectedBuffer = Buffer.from(expected, "utf8");
|
|
8406
|
-
const actualBuffer = Buffer.from(signatureHeader, "utf8");
|
|
8407
|
-
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
8408
|
-
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
8409
|
-
};
|
|
8410
|
-
program.command("listen").description("Start a local webhook listener for testing deliveries").option("--port <number>", "port to listen on", "8787").option("--path <path>", "request path to accept", "/webhook").option("--secret <secret>", "verify X-Soat-Signature with this webhook secret").option("--filter <pattern>", "filter event type(s), supports prefix wildcard and comma separation (e.g. sessions.generation.*,files.*)").option("--json", "print one JSON object per line").action(opts => {
|
|
8693
|
+
program.command("listen").description("Start a local webhook listener for testing deliveries").option("--port <number>", "port to listen on", "8787").option("--path <path>", "request path to accept", "/webhook").option("--secret <secret>", "verify the delivery signature with this webhook secret").option("--filter <pattern>", "filter event type(s), supports prefix wildcard and comma separation (e.g. sessions.generation.*,files.*)").option("--json", "print one JSON object per line").action(opts => {
|
|
8411
8694
|
const port = Number(opts.port);
|
|
8412
8695
|
const path = opts.path;
|
|
8413
8696
|
const secret = opts.secret;
|
|
@@ -8435,7 +8718,6 @@ program.command("listen").description("Start a local webhook listener for testin
|
|
|
8435
8718
|
const rawBody = Buffer.concat(chunks).toString("utf8");
|
|
8436
8719
|
const eventType = String(req.headers["x-soat-event"] ?? "unknown");
|
|
8437
8720
|
const deliveryId = String(req.headers["x-soat-delivery"] ?? "unknown");
|
|
8438
|
-
const signature = String(req.headers["x-soat-signature"] ?? "");
|
|
8439
8721
|
if (filter && !matchesFilter(eventType, filter)) {
|
|
8440
8722
|
res.writeHead(200, {
|
|
8441
8723
|
"Content-Type": "application/json"
|
|
@@ -8450,24 +8732,28 @@ program.command("listen").description("Start a local webhook listener for testin
|
|
|
8450
8732
|
try {
|
|
8451
8733
|
parsedPayload = JSON.parse(rawBody);
|
|
8452
8734
|
} catch {}
|
|
8453
|
-
|
|
8454
|
-
|
|
8735
|
+
const inspected = inspectDeliverySignature({
|
|
8736
|
+
secret,
|
|
8737
|
+
payload: rawBody,
|
|
8738
|
+
headers: req.headers
|
|
8739
|
+
});
|
|
8455
8740
|
const record = {
|
|
8456
8741
|
timestamp: (/* @__PURE__ */new Date()).toISOString(),
|
|
8457
8742
|
event_type: eventType,
|
|
8458
8743
|
delivery_id: deliveryId,
|
|
8459
|
-
signature,
|
|
8460
|
-
|
|
8744
|
+
signature: inspected.signature,
|
|
8745
|
+
signature_scheme: inspected.scheme,
|
|
8746
|
+
signature_valid: inspected.valid,
|
|
8461
8747
|
payload: parsedPayload
|
|
8462
8748
|
};
|
|
8463
8749
|
if (asJson) console.log(JSON.stringify(record));else {
|
|
8464
8750
|
console.log("--- webhook received ---");
|
|
8465
8751
|
console.log("event_type:", eventType);
|
|
8466
8752
|
console.log("delivery_id:", deliveryId);
|
|
8467
|
-
if (secret) console.log("signature_valid:",
|
|
8753
|
+
if (secret) console.log("signature_valid:", inspected.valid);
|
|
8468
8754
|
console.log("payload:", JSON.stringify(parsedPayload, null, 2));
|
|
8469
8755
|
}
|
|
8470
|
-
const responseStatus = secret &&
|
|
8756
|
+
const responseStatus = secret && inspected.valid === false ? 401 : 200;
|
|
8471
8757
|
res.writeHead(responseStatus, {
|
|
8472
8758
|
"Content-Type": "application/json"
|
|
8473
8759
|
});
|
|
@@ -8475,7 +8761,7 @@ program.command("listen").description("Start a local webhook listener for testin
|
|
|
8475
8761
|
ok: responseStatus === 200,
|
|
8476
8762
|
event_type: eventType,
|
|
8477
8763
|
delivery_id: deliveryId,
|
|
8478
|
-
signature_valid:
|
|
8764
|
+
signature_valid: inspected.valid
|
|
8479
8765
|
}));
|
|
8480
8766
|
});
|
|
8481
8767
|
});
|
|
@@ -8536,6 +8822,15 @@ program.argument("[command]", "API command in kebab-case (e.g. list-actors)").ar
|
|
|
8536
8822
|
const flagTypeByCanonical = new Map(route.flags.map(f => {
|
|
8537
8823
|
return [toCanonical(f.name), f.type];
|
|
8538
8824
|
}));
|
|
8825
|
+
const unknownFlagErrors = findUnknownFlags({
|
|
8826
|
+
commandName,
|
|
8827
|
+
route,
|
|
8828
|
+
flagKeys: Object.keys(flags)
|
|
8829
|
+
});
|
|
8830
|
+
if (unknownFlagErrors.length > 0) {
|
|
8831
|
+
for (const line of unknownFlagErrors) console.error(line);
|
|
8832
|
+
process.exit(1);
|
|
8833
|
+
}
|
|
8539
8834
|
const pathArgs = {};
|
|
8540
8835
|
const queryArgs = {};
|
|
8541
8836
|
const bodyArgs = {};
|
|
@@ -8604,6 +8899,14 @@ program.argument("[command]", "API command in kebab-case (e.g. list-actors)").ar
|
|
|
8604
8899
|
process.exit(1);
|
|
8605
8900
|
}
|
|
8606
8901
|
console.log(await formatResultData(result.data));
|
|
8902
|
+
const failure = resolveFailureMessage({
|
|
8903
|
+
commandName,
|
|
8904
|
+
data: result.data
|
|
8905
|
+
});
|
|
8906
|
+
if (failure) {
|
|
8907
|
+
console.error(failure);
|
|
8908
|
+
process.exit(1);
|
|
8909
|
+
}
|
|
8607
8910
|
});
|
|
8608
8911
|
var runCli = async args => {
|
|
8609
8912
|
const previousArgv = process.argv;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@inquirer/input": "^5.1.2",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"@ttoss/logger": "^0.8.19",
|
|
9
9
|
"commander": "^15.0.0",
|
|
10
10
|
"js-yaml": "^5.2.1",
|
|
11
|
-
"@soat/sdk": "0.
|
|
11
|
+
"@soat/sdk": "0.24.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@ttoss/config": "^1.37.17",
|