warpmetal 0.8.13 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -4
- package/docs/agent-insights-cli.md +123 -0
- package/docs/agent-work-cli.md +72 -0
- package/docs/session-handoff-transport.md +68 -0
- package/package.json +5 -3
- package/skills/warpmetal/SKILL.md +54 -1
- package/skills/warpmetal/references/cli-reference.md +150 -2
- package/skills/warpmetal/references/runtime.md +19 -0
- package/src/account-gateway.js +217 -0
- package/src/account-session.js +209 -0
- package/src/agent-insights.js +638 -0
- package/src/agent-management.js +146 -0
- package/src/agent-work.js +800 -0
- package/src/api.js +10 -4
- package/src/cli.js +398 -11
- package/src/contracts/agent-manager-control-v1.schema.json +563 -0
- package/src/contracts/http.js +3 -0
- package/src/contracts/insights.js +133 -0
- package/src/contracts/manager.js +86 -0
- package/src/contracts/session-handoff.js +49 -0
- package/src/contracts/sources.json +12 -0
- package/src/contracts/work-handoff.js +73 -0
- package/src/contracts/work.js +310 -0
- package/src/customer-auth.js +329 -0
- package/src/installer.js +5 -5
- package/src/models.js +85 -0
- package/src/runtime.js +76 -3
- package/src/session-handoff.js +915 -0
- package/src/state.js +20 -4
package/src/api.js
CHANGED
|
@@ -135,6 +135,10 @@ export class WarpMetalClient {
|
|
|
135
135
|
return this.request("GET", "/catalog");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
modelCatalog() {
|
|
139
|
+
return this.request("GET", "/agent-team-model-catalog");
|
|
140
|
+
}
|
|
141
|
+
|
|
138
142
|
prepareOrder(body, idempotencyKey) {
|
|
139
143
|
return this.request("POST", "/orders", { body, idempotencyKey });
|
|
140
144
|
}
|
|
@@ -376,11 +380,13 @@ export class WarpMetalClient {
|
|
|
376
380
|
);
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
sandboxAction(serverId, sandboxId, action, token, idempotencyKey) {
|
|
383
|
+
sandboxAction(serverId, sandboxId, action, token, idempotencyKey, imageDigest) {
|
|
380
384
|
const body =
|
|
381
|
-
action === "
|
|
382
|
-
? { action, confirm: "
|
|
383
|
-
:
|
|
385
|
+
action === "patch_image"
|
|
386
|
+
? { action, confirm: "patch_image", imageDigest }
|
|
387
|
+
: action === "refresh_image"
|
|
388
|
+
? { action, confirm: "refresh_image" }
|
|
389
|
+
: { action };
|
|
384
390
|
return this.request(
|
|
385
391
|
"POST",
|
|
386
392
|
`/servers/${encodeURIComponent(serverId)}/sandboxes/${encodeURIComponent(sandboxId)}/actions`,
|
package/src/cli.js
CHANGED
|
@@ -10,12 +10,24 @@ import {
|
|
|
10
10
|
rejectUnknownOptions,
|
|
11
11
|
stringOption,
|
|
12
12
|
} from "./args.js";
|
|
13
|
+
import {
|
|
14
|
+
AccountGatewayClient,
|
|
15
|
+
accountSessionContext,
|
|
16
|
+
authenticatedAccountClient,
|
|
17
|
+
} from "./account-gateway.js";
|
|
13
18
|
import { WarpMetalClient } from "./api.js";
|
|
19
|
+
import { managementClient, managementMutations, readManagementInput } from "./agent-management.js";
|
|
14
20
|
import { connectionProfile, writeConnectionProfile } from "./connection.js";
|
|
15
21
|
import { CliError, toErrorMessage } from "./errors.js";
|
|
16
22
|
import { establishHostTrust } from "./host-trust.js";
|
|
17
23
|
import { installSkill } from "./install-skill.js";
|
|
18
24
|
import { installRuntime } from "./installer.js";
|
|
25
|
+
import { openVerificationPage } from "./customer-auth.js";
|
|
26
|
+
import {
|
|
27
|
+
filterModelCatalog,
|
|
28
|
+
modelCatalogHuman,
|
|
29
|
+
MODEL_AUTH_MODES,
|
|
30
|
+
} from "./models.js";
|
|
19
31
|
import {
|
|
20
32
|
createPaymentRequestEnvelope,
|
|
21
33
|
defaultPaymentPaths,
|
|
@@ -74,12 +86,19 @@ const COMMON_OPTIONS = ["base-url", "json", "state-dir", "help"];
|
|
|
74
86
|
const HELP = `WarpMetal CLI ${VERSION}
|
|
75
87
|
|
|
76
88
|
Usage:
|
|
89
|
+
warpmetal login [--no-browser] [--read-only] [--json]
|
|
90
|
+
warpmetal auth status [--json]
|
|
91
|
+
warpmetal account orders [--task <taskId>] [--json]
|
|
92
|
+
warpmetal account devices [--server <serverId>] [--json]
|
|
93
|
+
warpmetal logout [--json]
|
|
77
94
|
warpmetal health [--json]
|
|
78
95
|
warpmetal catalog [--plan <planId>] [--json]
|
|
96
|
+
warpmetal models [--provider <id>] [--auth-mode <api_key|chatgpt_subscription>] [--json]
|
|
79
97
|
warpmetal order prepare --plan <planId> --hostname <name> --os <exact-name>
|
|
80
98
|
(--generate-ssh-key [--ssh-key-name <name>] | --ssh-public-key-file <path>)
|
|
81
99
|
[--runtime-file <path>] [--confirm TEMPORARY]
|
|
82
100
|
[--email <address>] [--idempotency-key <key>]
|
|
101
|
+
[--account [--without-agent-boxes | --without-team]]
|
|
83
102
|
warpmetal order status --task <taskId> [--wait] [--timeout-seconds <n>]
|
|
84
103
|
warpmetal checkout challenge --task <taskId> [--request-envelope-out <path>]
|
|
85
104
|
warpmetal checkout submit --task <taskId>
|
|
@@ -119,7 +138,8 @@ Usage:
|
|
|
119
138
|
[--lifetime temporary] [--expires-in-seconds <n>] [--confirm TEMPORARY] [--wait]
|
|
120
139
|
warpmetal sandbox create --server <serverId> --file <batch.json> [--confirm TEMPORARY]
|
|
121
140
|
warpmetal sandbox action --server <serverId> --sandbox <sandboxId>
|
|
122
|
-
--action <start|stop|restart|make_persistent|refresh_image> --confirm <same-action>
|
|
141
|
+
--action <start|stop|restart|make_persistent|refresh_image|patch_image> --confirm <same-action>
|
|
142
|
+
[--image-digest <image@sha256:digest> (patch_image only)] [--wait]
|
|
123
143
|
warpmetal sandbox list|get|action|delete ...
|
|
124
144
|
warpmetal sandbox access keygen --output <private-key-path> --confirm GENERATE
|
|
125
145
|
warpmetal sandbox access grant|list|get|revoke ...
|
|
@@ -133,11 +153,38 @@ Usage:
|
|
|
133
153
|
warpmetal tools install --server <serverId> --sandbox <sandboxId> --profile <profileId>
|
|
134
154
|
[--idempotency-key <key>] [--wait] [--timeout-seconds <n>]
|
|
135
155
|
warpmetal tools status --server <serverId> [--wait] [--timeout-seconds <n>]
|
|
156
|
+
warpmetal work list|sources|policy SERVER BOX [--json]
|
|
157
|
+
warpmetal work show|content|targets SERVER BOX WORK [--json]
|
|
158
|
+
warpmetal work create SERVER BOX --file <request.json>
|
|
159
|
+
warpmetal work update|enable|checkpoint|continue|restore|handoff SERVER BOX WORK --file <request.json>
|
|
160
|
+
warpmetal work status SERVER BOX WORK --kind <checkpoint|continue|restore|handoff>
|
|
161
|
+
(--request <requestId> | --operation <operationId>)
|
|
162
|
+
warpmetal work open SERVER BOX WORK [--connection-file <profile> --identity <sandbox-key> | --json]
|
|
163
|
+
warpmetal insights summary SERVER [--limit <n>] [--cursor <cursor>]
|
|
164
|
+
warpmetal insights status|list SERVER BOX [--json]
|
|
165
|
+
warpmetal insights enable|disable SERVER BOX --file <request.json>
|
|
166
|
+
warpmetal insights show SERVER BOX FINDING [--json]
|
|
167
|
+
warpmetal insights acknowledge|snooze|dismiss SERVER BOX FINDING --file <request.json>
|
|
168
|
+
warpmetal insights open SERVER BOX FINDING [--takeover <operationId>]
|
|
169
|
+
[--connection-file <profile> --identity <sandbox-key> | --json]
|
|
170
|
+
warpmetal insights manager settings SERVER BOX [--file <request.json>]
|
|
171
|
+
warpmetal insights manager activity SERVER BOX [--finding <findingId>]
|
|
172
|
+
warpmetal insights manager run SERVER BOX RUN
|
|
173
|
+
warpmetal insights manager target SERVER BOX FINDING
|
|
174
|
+
warpmetal insights manager recheck SERVER BOX FINDING --file <request.json>
|
|
175
|
+
warpmetal insights manager status SERVER BOX FINDING --request <requestId>
|
|
176
|
+
warpmetal insights takeover list SERVER BOX FINDING
|
|
177
|
+
warpmetal insights takeover SERVER BOX FINDING --file <request.json>
|
|
178
|
+
warpmetal insights takeover status SERVER BOX FINDING --operation <operationId>
|
|
179
|
+
warpmetal insights takeover resume SERVER BOX FINDING --operation <operationId> --file <request.json>
|
|
180
|
+
warpmetal insights review SERVER BOX RUN [--connection-file <profile> --identity <sandbox-key> | --json]
|
|
136
181
|
warpmetal state list
|
|
137
182
|
warpmetal agent install --target <codex|claude|all> [--scope <user|project>] [--force]
|
|
138
183
|
|
|
139
184
|
Global options:
|
|
140
185
|
--base-url <url> Override https://api.warpmetal.com
|
|
186
|
+
--identity-url <url> Override https://identity.warpmetal.com for account auth
|
|
187
|
+
--account-url <url> Override https://warpmetal.com for account requests
|
|
141
188
|
--state-dir <path> Override the private state directory
|
|
142
189
|
--json Emit structured, secret-redacted JSON
|
|
143
190
|
--help Show help
|
|
@@ -231,6 +278,18 @@ Sandbox SSH aliases:
|
|
|
231
278
|
https://geminicli.com/docs/get-started/installation/
|
|
232
279
|
https://geminicli.com/docs/cli/headless/
|
|
233
280
|
|
|
281
|
+
Retained Work and Insights:
|
|
282
|
+
Use an existing server owner/SSH login. Mutations take a closed request JSON
|
|
283
|
+
file with its request ID and revision fences. Repeating the same saved intent
|
|
284
|
+
performs GET-only reconciliation. Ordinary output contains metadata; work
|
|
285
|
+
content deliberately returns private owner text. Pending operations exit 8,
|
|
286
|
+
conflicts/terminal failures exit 5. Accepted continuation or handoff means
|
|
287
|
+
task admission, not task completion.
|
|
288
|
+
open/review --json returns a fresh exact descriptor. Interactive attachment
|
|
289
|
+
requires a sandbox grant/profile and local OpenCode 2.0.14. It creates no
|
|
290
|
+
session or initial prompt. Manager review is read-only. Recommend mode cannot
|
|
291
|
+
automatically steer a worker; protected takeover and Resume are explicit.
|
|
292
|
+
|
|
234
293
|
Credential environment variables:
|
|
235
294
|
WARPMETAL_OWNER_TOKEN Recovery/bootstrap credential for one explicit command
|
|
236
295
|
WARPMETAL_ACCESS_TOKEN Short-lived SSH-derived credential for one explicit command
|
|
@@ -827,6 +886,204 @@ async function handleCatalog(client, options, { json, stdout }) {
|
|
|
827
886
|
return 0;
|
|
828
887
|
}
|
|
829
888
|
|
|
889
|
+
async function handleModels(client, options, { json, stdout }) {
|
|
890
|
+
const provider = stringOption(options, "provider");
|
|
891
|
+
const authMode = stringOption(options, "auth-mode");
|
|
892
|
+
if (authMode !== undefined && !MODEL_AUTH_MODES.has(authMode)) {
|
|
893
|
+
throw new CliError(
|
|
894
|
+
"--auth-mode must be api_key or chatgpt_subscription.",
|
|
895
|
+
{ exitCode: 2 },
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
const result = await client.modelCatalog();
|
|
899
|
+
const data = filterModelCatalog(result.data, { provider, authMode });
|
|
900
|
+
emit(stdout, data, json, modelCatalogHuman(data));
|
|
901
|
+
return 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function accountOptions(options, context, stateDirectory) {
|
|
905
|
+
return {
|
|
906
|
+
stateDirectory,
|
|
907
|
+
identityOrigin: stringOption(options, "identity-url"),
|
|
908
|
+
accountOrigin: stringOption(options, "account-url"),
|
|
909
|
+
fetchImpl: context.fetchImpl,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function publicAccountIdentity(value) {
|
|
914
|
+
if (
|
|
915
|
+
!value ||
|
|
916
|
+
typeof value.principalId !== "string" ||
|
|
917
|
+
typeof value.email !== "string" ||
|
|
918
|
+
typeof value.emailVerified !== "boolean" ||
|
|
919
|
+
!Array.isArray(value.scopes) ||
|
|
920
|
+
value.scopes.some((scope) => !["cli:read", "cli:write"].includes(scope))
|
|
921
|
+
) {
|
|
922
|
+
throw new CliError("The account gateway returned an invalid identity response.", {
|
|
923
|
+
exitCode: 3,
|
|
924
|
+
code: "invalid_response",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
authenticated: true,
|
|
929
|
+
principalId: value.principalId,
|
|
930
|
+
email: value.email,
|
|
931
|
+
emailVerified: value.emailVerified,
|
|
932
|
+
scopes: value.scopes,
|
|
933
|
+
expiresAt:
|
|
934
|
+
typeof value.expiresAt === "string" ? value.expiresAt : undefined,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function handleAccountLogin(options, context, stateDirectory) {
|
|
939
|
+
const readOnlyValue = options["read-only"];
|
|
940
|
+
if (readOnlyValue !== undefined && typeof readOnlyValue !== "boolean") {
|
|
941
|
+
throw new CliError("--read-only does not take a value.", { exitCode: 2 });
|
|
942
|
+
}
|
|
943
|
+
if (readOnlyValue === false) {
|
|
944
|
+
throw new CliError("Use --read-only to request a read-only session.", {
|
|
945
|
+
exitCode: 2,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (options.browser === true) {
|
|
949
|
+
throw new CliError("Browser launch is automatic; use --no-browser to disable it.", {
|
|
950
|
+
exitCode: 2,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const account = accountSessionContext(
|
|
954
|
+
accountOptions(options, context, stateDirectory),
|
|
955
|
+
);
|
|
956
|
+
const scopes = readOnlyValue ? ["cli:read"] : ["cli:read", "cli:write"];
|
|
957
|
+
const controller = new AbortController();
|
|
958
|
+
const cancel = () => controller.abort();
|
|
959
|
+
process.once("SIGINT", cancel);
|
|
960
|
+
process.once("SIGTERM", cancel);
|
|
961
|
+
try {
|
|
962
|
+
const device = await account.auth.begin(scopes, {
|
|
963
|
+
signal: controller.signal,
|
|
964
|
+
});
|
|
965
|
+
writeLine(context.stderr, `Open ${device.verificationUriComplete}`);
|
|
966
|
+
writeLine(context.stderr, `Enter code: ${device.userCode}`);
|
|
967
|
+
if (options.browser !== false) {
|
|
968
|
+
openVerificationPage(device.verificationUriComplete, {
|
|
969
|
+
spawnImpl: context.spawnImpl,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
const session = await account.auth.poll(device, {
|
|
973
|
+
signal: controller.signal,
|
|
974
|
+
});
|
|
975
|
+
const identity = publicAccountIdentity(
|
|
976
|
+
await new AccountGatewayClient({
|
|
977
|
+
accountOrigin: account.accountOrigin,
|
|
978
|
+
accessToken: session.accessToken,
|
|
979
|
+
fetchImpl: context.fetchImpl,
|
|
980
|
+
}).whoami(),
|
|
981
|
+
);
|
|
982
|
+
await account.store.withLock(async () => {
|
|
983
|
+
await account.store.write({
|
|
984
|
+
...session,
|
|
985
|
+
createdAt: new Date().toISOString(),
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
emit(
|
|
989
|
+
context.stdout,
|
|
990
|
+
identity,
|
|
991
|
+
context.json,
|
|
992
|
+
`Authenticated as ${identity.email} (${identity.scopes.join(", ")}).`,
|
|
993
|
+
);
|
|
994
|
+
return 0;
|
|
995
|
+
} finally {
|
|
996
|
+
process.removeListener("SIGINT", cancel);
|
|
997
|
+
process.removeListener("SIGTERM", cancel);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function handleAccountStatus(options, context, stateDirectory) {
|
|
1002
|
+
const authenticated = await authenticatedAccountClient(
|
|
1003
|
+
accountOptions(options, context, stateDirectory),
|
|
1004
|
+
);
|
|
1005
|
+
if (!authenticated) {
|
|
1006
|
+
emit(
|
|
1007
|
+
context.stdout,
|
|
1008
|
+
{ authenticated: false },
|
|
1009
|
+
context.json,
|
|
1010
|
+
"Not authenticated. Run warpmetal login.",
|
|
1011
|
+
);
|
|
1012
|
+
return 0;
|
|
1013
|
+
}
|
|
1014
|
+
const identity = publicAccountIdentity(await authenticated.client.whoami());
|
|
1015
|
+
emit(
|
|
1016
|
+
context.stdout,
|
|
1017
|
+
identity,
|
|
1018
|
+
context.json,
|
|
1019
|
+
`Authenticated as ${identity.email} (${identity.scopes.join(", ")}).`,
|
|
1020
|
+
);
|
|
1021
|
+
return 0;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function requireAccount(options, context, stateDirectory) {
|
|
1025
|
+
const account = await authenticatedAccountClient(accountOptions(options, context, stateDirectory));
|
|
1026
|
+
if (!account) {
|
|
1027
|
+
throw new CliError("Sign in first with warpmetal login; no order details are needed to sign in.", { exitCode: 4 });
|
|
1028
|
+
}
|
|
1029
|
+
return account;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function handleAccountInventory(kind, options, context, stateDirectory) {
|
|
1033
|
+
const account = await requireAccount(options, context, stateDirectory);
|
|
1034
|
+
const id = stringOption(options, kind === "orders" ? "task" : "server");
|
|
1035
|
+
const data = kind === "orders"
|
|
1036
|
+
? await (id ? account.client.order(id) : account.client.orders())
|
|
1037
|
+
: await (id ? account.client.device(id) : account.client.devices());
|
|
1038
|
+
emit(context.stdout, data, context.json, JSON.stringify(data, null, 2));
|
|
1039
|
+
return 0;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function handleAccountLogout(options, context, stateDirectory) {
|
|
1043
|
+
const account = accountSessionContext(
|
|
1044
|
+
accountOptions(options, context, stateDirectory),
|
|
1045
|
+
);
|
|
1046
|
+
const outcome = await account.store.withLock(async () => {
|
|
1047
|
+
const session = await account.store.read();
|
|
1048
|
+
if (!session) {
|
|
1049
|
+
return {
|
|
1050
|
+
authenticated: false,
|
|
1051
|
+
localSessionCleared: false,
|
|
1052
|
+
remoteRevoked: false,
|
|
1053
|
+
remoteStatus: "no_session",
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
let remoteRevoked = false;
|
|
1057
|
+
let remoteError;
|
|
1058
|
+
try {
|
|
1059
|
+
await account.auth.revoke(session.refreshToken);
|
|
1060
|
+
remoteRevoked = true;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
remoteError = error?.code || "unavailable";
|
|
1063
|
+
} finally {
|
|
1064
|
+
await account.store.clear();
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
authenticated: false,
|
|
1068
|
+
localSessionCleared: true,
|
|
1069
|
+
remoteRevoked,
|
|
1070
|
+
remoteStatus: remoteRevoked ? "revoked" : "failed",
|
|
1071
|
+
remoteError,
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
emit(
|
|
1075
|
+
context.stdout,
|
|
1076
|
+
outcome,
|
|
1077
|
+
context.json,
|
|
1078
|
+
outcome.remoteRevoked
|
|
1079
|
+
? "Logged out and revoked the remote session."
|
|
1080
|
+
: outcome.remoteStatus === "no_session"
|
|
1081
|
+
? "No account session is stored for these origins."
|
|
1082
|
+
: "Local session cleared, but remote revocation could not be confirmed.",
|
|
1083
|
+
);
|
|
1084
|
+
return outcome.remoteStatus === "failed" ? 3 : 0;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
830
1087
|
async function pathExists(path) {
|
|
831
1088
|
try {
|
|
832
1089
|
await stat(path);
|
|
@@ -898,6 +1155,20 @@ async function resolveServerIdentity(store, serverId, explicit) {
|
|
|
898
1155
|
}
|
|
899
1156
|
|
|
900
1157
|
async function handlePrepareOrder(client, store, options, context) {
|
|
1158
|
+
const accountMode = booleanOption(options, "account");
|
|
1159
|
+
const account = accountMode ? await requireAccount(options, context, store.directory) : null;
|
|
1160
|
+
const identity = account ? publicAccountIdentity(await account.client.whoami()) : null;
|
|
1161
|
+
if (account && !account.session.scopes.includes("cli:write")) {
|
|
1162
|
+
throw new CliError("This account session is read-only. Run warpmetal login to request order preparation.", { exitCode: 4 });
|
|
1163
|
+
}
|
|
1164
|
+
const withoutBoxes = booleanOption(options, "without-agent-boxes");
|
|
1165
|
+
const withoutTeam = booleanOption(options, "without-team");
|
|
1166
|
+
if (!account && (withoutBoxes || withoutTeam || options["identity-url"] || options["account-url"])) {
|
|
1167
|
+
throw new CliError("Account options require --account. Existing owner-token preparation is unchanged.", { exitCode: 2 });
|
|
1168
|
+
}
|
|
1169
|
+
if (withoutBoxes && withoutTeam) {
|
|
1170
|
+
throw new CliError("Use only one of --without-agent-boxes or --without-team.", { exitCode: 2 });
|
|
1171
|
+
}
|
|
901
1172
|
const planId = stringOption(options, "plan", { required: true });
|
|
902
1173
|
const hostname = stringOption(options, "hostname", { required: true });
|
|
903
1174
|
const osName = stringOption(options, "os", { required: true });
|
|
@@ -910,10 +1181,16 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
910
1181
|
);
|
|
911
1182
|
}
|
|
912
1183
|
const email = stringOption(options, "email");
|
|
1184
|
+
if (account && email) throw new CliError("Account contact comes from your signed-in account; omit --email.", { exitCode: 2 });
|
|
913
1185
|
const runtimeFile = stringOption(options, "runtime-file");
|
|
1186
|
+
if (runtimeFile && withoutBoxes) throw new CliError("--runtime-file conflicts with --without-agent-boxes.", { exitCode: 2 });
|
|
1187
|
+
if (account && !runtimeFile && !withoutBoxes && !withoutTeam) {
|
|
1188
|
+
throw new CliError("Agent Boxes and Agent team are enabled by default. Select your team in --runtime-file, or use --without-team / --without-agent-boxes.", { exitCode: 2 });
|
|
1189
|
+
}
|
|
914
1190
|
const runtimeIntent = runtimeFile
|
|
915
1191
|
? await readSandboxFile(runtimeFile)
|
|
916
|
-
: undefined;
|
|
1192
|
+
: account && withoutTeam ? [{ name: "main", size: "small" }] : undefined;
|
|
1193
|
+
if (withoutTeam && runtimeIntent?.teams) throw new CliError("--without-team conflicts with teams in --runtime-file.", { exitCode: 2 });
|
|
917
1194
|
const sandboxes = Array.isArray(runtimeIntent)
|
|
918
1195
|
? runtimeIntent
|
|
919
1196
|
: runtimeIntent?.sandboxes;
|
|
@@ -969,14 +1246,22 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
969
1246
|
}
|
|
970
1247
|
const key =
|
|
971
1248
|
stringOption(options, "idempotency-key") || idempotencyKey("order");
|
|
972
|
-
const response =
|
|
1249
|
+
const response = account
|
|
1250
|
+
? { data: await account.client.prepareOrder(request, key) }
|
|
1251
|
+
: await client.prepareOrder(request, key);
|
|
973
1252
|
const checkoutBody = JSON.stringify({ taskId: response.data.task.id });
|
|
974
1253
|
await store.savePreparedOrder(
|
|
975
1254
|
response.data,
|
|
976
1255
|
checkoutBody,
|
|
977
1256
|
selected.identity.identityId,
|
|
1257
|
+
account ? { principalId: identity.principalId, accountOrigin: account.context.accountOrigin, identityOrigin: account.context.identityOrigin } : null,
|
|
978
1258
|
);
|
|
979
1259
|
const safe = safePreparedOrder(response.data, store.path);
|
|
1260
|
+
if (account) {
|
|
1261
|
+
safe.ownershipMode = "account";
|
|
1262
|
+
safe.credential = { kind: "account_session", stored: true, printed: false };
|
|
1263
|
+
safe.stateFile = store.path;
|
|
1264
|
+
}
|
|
980
1265
|
if (runtimeIntent) {
|
|
981
1266
|
safe.agentRuntimeIntent = {
|
|
982
1267
|
sandboxes: sandboxes.map((sandbox) => ({
|
|
@@ -988,13 +1273,16 @@ async function handlePrepareOrder(client, store, options, context) {
|
|
|
988
1273
|
};
|
|
989
1274
|
if (!Array.isArray(runtimeIntent)) {
|
|
990
1275
|
safe.agentRuntimeIntent.setup = runtimeIntent.setup;
|
|
1276
|
+
safe.agentRuntimeIntent.teams = runtimeIntent.teams;
|
|
991
1277
|
}
|
|
992
1278
|
}
|
|
993
1279
|
emit(
|
|
994
1280
|
context.stdout,
|
|
995
1281
|
safe,
|
|
996
1282
|
context.json,
|
|
997
|
-
|
|
1283
|
+
account
|
|
1284
|
+
? `Prepared ${safe.task.id} for your account. Ownership recorded in ${store.path}. No owner token was issued. Use warpmetal account orders --task ${safe.task.id} to view it.`
|
|
1285
|
+
: `Prepared ${safe.task.id} for server ${safe.task.serverId}.\nRecovery credential saved to ${store.path} and not printed.`,
|
|
998
1286
|
);
|
|
999
1287
|
return 0;
|
|
1000
1288
|
}
|
|
@@ -2431,7 +2719,13 @@ async function resolveCreateSandboxes(options) {
|
|
|
2431
2719
|
exitCode: 2,
|
|
2432
2720
|
});
|
|
2433
2721
|
}
|
|
2434
|
-
if (file)
|
|
2722
|
+
if (file) {
|
|
2723
|
+
const intent = await readSandboxFile(file);
|
|
2724
|
+
if (!Array.isArray(intent)) {
|
|
2725
|
+
throw new CliError("Team and setup intents are supported only during order preparation; sandbox create accepts sandboxes only.", { exitCode: 2 });
|
|
2726
|
+
}
|
|
2727
|
+
return intent;
|
|
2728
|
+
}
|
|
2435
2729
|
return sandboxFromOptions({
|
|
2436
2730
|
name: stringOption(options, "name", { required: true }),
|
|
2437
2731
|
size: stringOption(options, "size", { required: true }),
|
|
@@ -2528,6 +2822,18 @@ async function handleSandboxGet(client, store, options, context) {
|
|
|
2528
2822
|
return result.data.sandbox.observedState === "failed" ? 5 : 0;
|
|
2529
2823
|
}
|
|
2530
2824
|
|
|
2825
|
+
function validateImmutableImageReference(reference) {
|
|
2826
|
+
// Keep the canonical backend runtime.IMAGE_DIGEST full-match semantics.
|
|
2827
|
+
const match = /^[a-z0-9][a-z0-9._:\/-]*@sha256:[a-f0-9]{64}$/.exec(reference);
|
|
2828
|
+
if (match?.[0] !== reference) {
|
|
2829
|
+
throw new CliError(
|
|
2830
|
+
"--image-digest requires an immutable digest-pinned image reference ending in @sha256:<64 lowercase hex characters>.",
|
|
2831
|
+
{ exitCode: 2 },
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
return reference;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2531
2837
|
async function handleSandboxAction(client, store, options, context) {
|
|
2532
2838
|
const serverId = stringOption(options, "server", { required: true });
|
|
2533
2839
|
const sandboxId = stringOption(options, "sandbox", { required: true });
|
|
@@ -2539,10 +2845,11 @@ async function handleSandboxAction(client, store, options, context) {
|
|
|
2539
2845
|
"restart",
|
|
2540
2846
|
"make_persistent",
|
|
2541
2847
|
"refresh_image",
|
|
2848
|
+
"patch_image",
|
|
2542
2849
|
].includes(action)
|
|
2543
2850
|
) {
|
|
2544
2851
|
throw new CliError(
|
|
2545
|
-
"--action must be start, stop, restart, make_persistent, or
|
|
2852
|
+
"--action must be start, stop, restart, make_persistent, refresh_image, or patch_image.",
|
|
2546
2853
|
{
|
|
2547
2854
|
exitCode: 2,
|
|
2548
2855
|
},
|
|
@@ -2553,6 +2860,17 @@ async function handleSandboxAction(client, store, options, context) {
|
|
|
2553
2860
|
exitCode: 2,
|
|
2554
2861
|
});
|
|
2555
2862
|
}
|
|
2863
|
+
if (action !== "patch_image" && options["image-digest"] !== undefined) {
|
|
2864
|
+
throw new CliError("--image-digest is only supported with --action patch_image.", {
|
|
2865
|
+
exitCode: 2,
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
const imageDigest =
|
|
2869
|
+
action === "patch_image"
|
|
2870
|
+
? validateImmutableImageReference(
|
|
2871
|
+
stringOption(options, "image-digest", { required: true }),
|
|
2872
|
+
)
|
|
2873
|
+
: undefined;
|
|
2556
2874
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
2557
2875
|
const key =
|
|
2558
2876
|
stringOption(options, "idempotency-key") ||
|
|
@@ -2563,14 +2881,18 @@ async function handleSandboxAction(client, store, options, context) {
|
|
|
2563
2881
|
action,
|
|
2564
2882
|
token,
|
|
2565
2883
|
key,
|
|
2884
|
+
imageDigest,
|
|
2566
2885
|
);
|
|
2567
2886
|
if (booleanOption(options, "wait")) {
|
|
2568
2887
|
const accepted = result.data.sandbox;
|
|
2569
|
-
|
|
2888
|
+
const imageAction = action === "refresh_image" || action === "patch_image";
|
|
2889
|
+
if (imageAction && !accepted.desiredImageDigest) {
|
|
2570
2890
|
throw new CliError(
|
|
2571
|
-
|
|
2891
|
+
`WarpMetal accepted image ${action === "patch_image" ? "patch" : "refresh"} without an immutable desired image digest.`,
|
|
2572
2892
|
);
|
|
2573
2893
|
}
|
|
2894
|
+
const expectedImageDigest =
|
|
2895
|
+
action === "patch_image" ? imageDigest : accepted.desiredImageDigest;
|
|
2574
2896
|
const expectedState =
|
|
2575
2897
|
accepted.desiredState === "stopped" ? "stopped" : "running";
|
|
2576
2898
|
result = await pollSandbox(
|
|
@@ -2582,8 +2904,7 @@ async function handleSandboxAction(client, store, options, context) {
|
|
|
2582
2904
|
(sandbox) =>
|
|
2583
2905
|
sandbox.observedState === expectedState &&
|
|
2584
2906
|
Number(sandbox.observedGeneration) >= Number(accepted.generation) &&
|
|
2585
|
-
(
|
|
2586
|
-
sandbox.imageDigest === accepted.desiredImageDigest),
|
|
2907
|
+
(!imageAction || sandbox.imageDigest === expectedImageDigest),
|
|
2587
2908
|
);
|
|
2588
2909
|
}
|
|
2589
2910
|
await store.saveSandboxes(serverId, [result.data.sandbox]);
|
|
@@ -3068,16 +3389,69 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3068
3389
|
return handleAccessRemoveSsh(options, context);
|
|
3069
3390
|
}
|
|
3070
3391
|
|
|
3071
|
-
const baseUrl = stringOption(options, "base-url");
|
|
3072
3392
|
const stateDir =
|
|
3073
3393
|
stringOption(options, "state-dir") ||
|
|
3074
3394
|
resolveStateDirectory({ env: context.env });
|
|
3395
|
+
if (command === "login") {
|
|
3396
|
+
rejectUnknownOptions(options, [
|
|
3397
|
+
"base-url",
|
|
3398
|
+
"json",
|
|
3399
|
+
"help",
|
|
3400
|
+
"state-dir",
|
|
3401
|
+
"identity-url",
|
|
3402
|
+
"account-url",
|
|
3403
|
+
"browser",
|
|
3404
|
+
"read-only",
|
|
3405
|
+
]);
|
|
3406
|
+
return handleAccountLogin(options, context, stateDir);
|
|
3407
|
+
}
|
|
3408
|
+
if (command === "auth status") {
|
|
3409
|
+
rejectUnknownOptions(options, [
|
|
3410
|
+
"base-url",
|
|
3411
|
+
"json",
|
|
3412
|
+
"help",
|
|
3413
|
+
"state-dir",
|
|
3414
|
+
"identity-url",
|
|
3415
|
+
"account-url",
|
|
3416
|
+
]);
|
|
3417
|
+
return handleAccountStatus(options, context, stateDir);
|
|
3418
|
+
}
|
|
3419
|
+
if (command === "logout") {
|
|
3420
|
+
rejectUnknownOptions(options, [
|
|
3421
|
+
"base-url",
|
|
3422
|
+
"json",
|
|
3423
|
+
"help",
|
|
3424
|
+
"state-dir",
|
|
3425
|
+
"identity-url",
|
|
3426
|
+
"account-url",
|
|
3427
|
+
]);
|
|
3428
|
+
return handleAccountLogout(options, context, stateDir);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
const baseUrl = stringOption(options, "base-url");
|
|
3075
3432
|
const client = new WarpMetalClient({
|
|
3076
3433
|
baseUrl: baseUrl || context.env.WARPMETAL_API_URL,
|
|
3077
3434
|
fetchImpl: context.fetchImpl,
|
|
3078
3435
|
});
|
|
3079
3436
|
const store = new StateStore(stateDir);
|
|
3080
3437
|
|
|
3438
|
+
if (positionals[0] === "work" || positionals[0] === "insights") {
|
|
3439
|
+
const scopedClient = managementClient({
|
|
3440
|
+
baseUrl: baseUrl || context.env.WARPMETAL_API_URL,
|
|
3441
|
+
fetchImpl: context.fetchImpl,
|
|
3442
|
+
});
|
|
3443
|
+
const services = {
|
|
3444
|
+
client: scopedClient, store, context, requireServerToken, emit,
|
|
3445
|
+
readInput: readManagementInput,
|
|
3446
|
+
runMutation: managementMutations(stateDir, scopedClient.baseUrl),
|
|
3447
|
+
openSessionHandoff: async (...args) => (await import("./session-handoff.js")).openSessionHandoff(...args),
|
|
3448
|
+
};
|
|
3449
|
+
if (positionals[0] === "work") {
|
|
3450
|
+
return (await import("./agent-work.js")).handleWork(positionals, options, services);
|
|
3451
|
+
}
|
|
3452
|
+
return (await import("./agent-insights.js")).handleInsights(positionals, options, services);
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3081
3455
|
switch (command) {
|
|
3082
3456
|
case "health":
|
|
3083
3457
|
rejectUnknownOptions(options, COMMON_OPTIONS);
|
|
@@ -3085,9 +3459,21 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3085
3459
|
case "catalog":
|
|
3086
3460
|
rejectUnknownOptions(options, [...COMMON_OPTIONS, "plan"]);
|
|
3087
3461
|
return handleCatalog(client, options, context);
|
|
3462
|
+
case "account orders":
|
|
3463
|
+
case "account devices":
|
|
3464
|
+
rejectUnknownOptions(options, [...COMMON_OPTIONS, "identity-url", "account-url", command === "account orders" ? "task" : "server"]);
|
|
3465
|
+
return handleAccountInventory(command === "account orders" ? "orders" : "devices", options, context, stateDir);
|
|
3466
|
+
case "models":
|
|
3467
|
+
rejectUnknownOptions(options, [...COMMON_OPTIONS, "provider", "auth-mode"]);
|
|
3468
|
+
return handleModels(client, options, context);
|
|
3088
3469
|
case "order prepare":
|
|
3089
3470
|
rejectUnknownOptions(options, [
|
|
3090
3471
|
...COMMON_OPTIONS,
|
|
3472
|
+
"account",
|
|
3473
|
+
"identity-url",
|
|
3474
|
+
"account-url",
|
|
3475
|
+
"without-agent-boxes",
|
|
3476
|
+
"without-team",
|
|
3091
3477
|
"plan",
|
|
3092
3478
|
"hostname",
|
|
3093
3479
|
"os",
|
|
@@ -3376,6 +3762,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
3376
3762
|
"sandbox",
|
|
3377
3763
|
"token-file",
|
|
3378
3764
|
"action",
|
|
3765
|
+
"image-digest",
|
|
3379
3766
|
"confirm",
|
|
3380
3767
|
"idempotency-key",
|
|
3381
3768
|
"wait",
|