warpmetal 0.2.3 → 0.3.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 +9 -3
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +26 -7
- package/skills/warpmetal/references/cli-reference.md +17 -4
- package/skills/warpmetal/references/runtime.md +19 -0
- package/skills/warpmetal/references/safety.md +9 -2
- package/src/api.js +12 -0
- package/src/cli.js +209 -1
- package/src/state.js +14 -0
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ The official command-line client and portable Agent Skill for WarpMetal.
|
|
|
4
4
|
|
|
5
5
|
The CLI uses the public API at `https://api.warpmetal.com`, stores generated
|
|
6
6
|
WarpMetal credentials in a user-private state file, and never reads or stores
|
|
7
|
-
wallet private keys or SSH private-key contents. Version 0.
|
|
8
|
-
Agent Runtime
|
|
7
|
+
wallet private keys or SSH private-key contents. Version 0.3 adds guarded OS
|
|
8
|
+
reload and Agent Runtime recovery to the fixed-size, isolated sandbox workflow.
|
|
9
9
|
|
|
10
10
|
## Distribution
|
|
11
11
|
|
|
@@ -69,7 +69,7 @@ alternate state directory.
|
|
|
69
69
|
every OS reload. Password and keyboard-interactive login are disabled; never
|
|
70
70
|
request, store, or expect a VPS login password.
|
|
71
71
|
- The CLI accepts an externally produced x402 `PAYMENT-SIGNATURE` from a file.
|
|
72
|
-
|
|
72
|
+
Wallet key management and signing remain outside this package.
|
|
73
73
|
- Destructive or state-changing commands require explicit confirmations and
|
|
74
74
|
generate idempotency keys by default.
|
|
75
75
|
- Runtime bootstrap credentials remain memory-only. Signed supervisor bundles
|
|
@@ -80,6 +80,12 @@ alternate state directory.
|
|
|
80
80
|
- Sandboxes use the fixed runtime image and fixed sizes. Persistent is the
|
|
81
81
|
default; temporary sandboxes require explicit confirmation and permanently
|
|
82
82
|
delete their workspace after 15 minutes to 24 hours.
|
|
83
|
+
- Guarded reload powers the server off first. Runtime-enabled reload requires a
|
|
84
|
+
second acknowledgment, after which the CLI guides supervisor reinstall and
|
|
85
|
+
pinned connection-profile refresh. Post-reload owner SSH host keys must be
|
|
86
|
+
independently verified because the provider may rotate or preserve them;
|
|
87
|
+
update `known_hosts` only when the verified key changed. Erased workspaces
|
|
88
|
+
are never described as recoverable.
|
|
83
89
|
|
|
84
90
|
## Agent Runtime example
|
|
85
91
|
|
package/package.json
CHANGED
|
@@ -118,13 +118,32 @@ warpmetal server power \
|
|
|
118
118
|
--json
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
For a destructive reload, explain that every server-disk file is erased and
|
|
122
|
+
obtain explicit approval. If Agent Runtime is enabled, also explain that every
|
|
123
|
+
sandbox workspace is lost, desired sandboxes return as empty workspaces after
|
|
124
|
+
reinstall, and pinned profiles must be refreshed. Then use only the guarded
|
|
125
|
+
command:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
warpmetal server reload \
|
|
129
|
+
--server <serverId> \
|
|
130
|
+
--confirm ERASE \
|
|
131
|
+
--power-off-first \
|
|
132
|
+
[--acknowledge-agent-runtime-reset] \
|
|
133
|
+
[--os '<exact-live-os-name>'] \
|
|
134
|
+
--wait \
|
|
135
|
+
--json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The CLI requires the recovery owner credential so it can keep polling after
|
|
139
|
+
reload revokes SSH-derived access tokens. On success, reinstall Agent Runtime
|
|
140
|
+
only after verifying the post-reload owner-facing SSH host-key fingerprint
|
|
141
|
+
through a trusted provider or console channel. The provider may rotate or
|
|
142
|
+
preserve the key; update `known_hosts` only when the verified key changed and
|
|
143
|
+
never bypass a mismatch. Then wait for grants to become applied and refresh
|
|
144
|
+
every connection profile with `sandbox access refresh --confirm REFRESH`
|
|
145
|
+
before connecting. Do not fall back to raw API calls for deletion, networking,
|
|
146
|
+
renewal, or another unsupported mutation.
|
|
128
147
|
|
|
129
148
|
## Use Agent Runtime
|
|
130
149
|
|
|
@@ -68,6 +68,12 @@ warpmetal server power \
|
|
|
68
68
|
[--wait] [--timeout-seconds <n>] \
|
|
69
69
|
--json
|
|
70
70
|
|
|
71
|
+
warpmetal server reload \
|
|
72
|
+
--server <serverId> --confirm ERASE --power-off-first \
|
|
73
|
+
[--acknowledge-agent-runtime-reset] [--hostname <name>] \
|
|
74
|
+
[--os <exact-live-os-name>] [--ssh-public-key-file <public-key-path>] \
|
|
75
|
+
[--idempotency-key <key>] [--wait] [--timeout-seconds <n>] --json
|
|
76
|
+
|
|
71
77
|
warpmetal operation get \
|
|
72
78
|
--operation <operationId> \
|
|
73
79
|
[--server <serverId>] \
|
|
@@ -75,10 +81,11 @@ warpmetal operation get \
|
|
|
75
81
|
--json
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
verification
|
|
81
|
-
|
|
84
|
+
Reload requires the recovery owner credential rather than a short-lived
|
|
85
|
+
SSH-derived token. `--power-off-first` authorizes shutdown and powered-off
|
|
86
|
+
verification inside the same operation. When Agent Runtime is enabled,
|
|
87
|
+
`--acknowledge-agent-runtime-reset` is required because workspaces are erased,
|
|
88
|
+
the supervisor must be reinstalled, and connection profiles must be refreshed.
|
|
82
89
|
|
|
83
90
|
Use `--token-file` only for recovery when local state is unavailable. Prefer
|
|
84
91
|
`WARPMETAL_OWNER_TOKEN` or `WARPMETAL_ACCESS_TOKEN` for a single command over a
|
|
@@ -124,6 +131,9 @@ warpmetal sandbox access grant \
|
|
|
124
131
|
warpmetal sandbox access list --server <serverId> --sandbox <sandboxId> --json
|
|
125
132
|
warpmetal sandbox access get \
|
|
126
133
|
--server <serverId> --sandbox <sandboxId> --grant <grantId> [--wait] --json
|
|
134
|
+
warpmetal sandbox access refresh \
|
|
135
|
+
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
136
|
+
--connection-file <profile-path> --confirm REFRESH [--wait] --json
|
|
127
137
|
warpmetal sandbox access revoke \
|
|
128
138
|
--server <serverId> --sandbox <sandboxId> --grant <grantId> \
|
|
129
139
|
--confirm REVOKE [--wait] --json
|
|
@@ -134,6 +144,9 @@ warpmetal sandbox connect --connection-file <profile-path> \
|
|
|
134
144
|
`sandbox connect` is the only runtime command that does not use `--json`; it
|
|
135
145
|
returns the OpenSSH or remote exit status. `--connection-file` on grant
|
|
136
146
|
creation requires `--wait`.
|
|
147
|
+
`sandbox access refresh` atomically replaces a stale token-free profile with
|
|
148
|
+
the currently applied grant and API-reported pinned host keys; use it after an
|
|
149
|
+
OS reload and supervisor reinstall.
|
|
137
150
|
|
|
138
151
|
## Skill installation and state
|
|
139
152
|
|
|
@@ -145,6 +145,25 @@ The token-free profile is written only after the grant is `applied` and the
|
|
|
145
145
|
API supplies verified VPS host keys. Do not print or open that profile in an
|
|
146
146
|
agent conversation.
|
|
147
147
|
|
|
148
|
+
After a destructive OS reload, reinstall the supervisor and wait for the
|
|
149
|
+
retained active grant to become `applied`, then replace its stale pinned
|
|
150
|
+
profile:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
warpmetal sandbox access refresh \
|
|
154
|
+
--server <serverId> \
|
|
155
|
+
--sandbox <sandboxId> \
|
|
156
|
+
--grant <grantId> \
|
|
157
|
+
--connection-file <profile-path> \
|
|
158
|
+
--confirm REFRESH \
|
|
159
|
+
--wait \
|
|
160
|
+
--json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The sandbox record is retained, but its old workspace is not; reconciliation
|
|
164
|
+
creates a new empty workspace. Never bypass a host-key mismatch or reuse the
|
|
165
|
+
pre-reload profile.
|
|
166
|
+
|
|
148
167
|
Connect without an owner management credential:
|
|
149
168
|
|
|
150
169
|
```sh
|
|
@@ -54,8 +54,15 @@ Obtain explicit user approval immediately before:
|
|
|
54
54
|
A reload requires both `confirm: "ERASE"` and `powerOffFirst: true`. Treat
|
|
55
55
|
`powerOffFirst` as explicit authorization for WarpMetal to shut down the
|
|
56
56
|
server, wait until it is powered off, and then erase and reinstall it inside
|
|
57
|
-
one lifecycle operation.
|
|
58
|
-
|
|
57
|
+
one lifecycle operation. When Agent Runtime is enabled, also require
|
|
58
|
+
`acknowledgeAgentRuntimeReset`: all sandbox workspaces are permanently erased,
|
|
59
|
+
the supervisor identity is revoked, desired sandboxes are recreated empty
|
|
60
|
+
after reinstall, and all pinned profiles require refresh. Use only the guarded
|
|
61
|
+
CLI command and stop on `manual_review`. Reload invalidates the prior
|
|
62
|
+
owner-facing SSH host-key trust decision; the provider may rotate or preserve
|
|
63
|
+
the key. Verify the post-reload fingerprint through a trusted provider or
|
|
64
|
+
console channel, change `known_hosts` only when the verified key changed, and
|
|
65
|
+
never disable host-key checking.
|
|
59
66
|
|
|
60
67
|
An order preparation is unpaid but consumes a limited prepared-order slot.
|
|
61
68
|
Confirm the plan, hostname, OS, and public key before preparing it.
|
package/src/api.js
CHANGED
|
@@ -188,6 +188,18 @@ export class WarpMetalClient {
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
reloadServer(serverId, body, token, idempotencyKey) {
|
|
192
|
+
return this.request(
|
|
193
|
+
"POST",
|
|
194
|
+
`/api/servers/${encodeURIComponent(serverId)}/reload`,
|
|
195
|
+
{
|
|
196
|
+
body,
|
|
197
|
+
token,
|
|
198
|
+
idempotencyKey,
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
getOperation(operationId, token) {
|
|
192
204
|
return this.request(
|
|
193
205
|
"GET",
|
package/src/cli.js
CHANGED
|
@@ -73,6 +73,9 @@ Usage:
|
|
|
73
73
|
warpmetal server get --server <serverId>
|
|
74
74
|
warpmetal server power --server <serverId> --action <boot|reboot|shutdown>
|
|
75
75
|
--confirm <same-action> [--wait] [--idempotency-key <key>]
|
|
76
|
+
warpmetal server reload --server <serverId> --confirm ERASE --power-off-first
|
|
77
|
+
[--acknowledge-agent-runtime-reset] [--hostname <name>] [--os <exact-name>]
|
|
78
|
+
[--ssh-public-key-file <path>] [--wait] [--idempotency-key <key>]
|
|
76
79
|
warpmetal operation get --operation <operationId> [--server <serverId>] [--wait]
|
|
77
80
|
warpmetal runtime enable|get --server <serverId> [--wait]
|
|
78
81
|
warpmetal runtime install --server <serverId> --identity <owner-key> --ssh-user <user>
|
|
@@ -83,6 +86,8 @@ Usage:
|
|
|
83
86
|
warpmetal sandbox list|get|action|delete ...
|
|
84
87
|
warpmetal sandbox access keygen --output <private-key-path> --confirm GENERATE
|
|
85
88
|
warpmetal sandbox access grant|list|get|revoke ...
|
|
89
|
+
warpmetal sandbox access refresh --server <serverId> --sandbox <sandboxId>
|
|
90
|
+
--grant <grantId> --connection-file <path> --confirm REFRESH [--wait]
|
|
86
91
|
warpmetal sandbox connect --connection-file <path> --identity <sandbox-key> [-- <command>]
|
|
87
92
|
warpmetal state list
|
|
88
93
|
warpmetal agent install --target <codex|claude|all> [--scope <user|project>] [--force]
|
|
@@ -199,6 +204,19 @@ async function requireServerToken(store, serverId, options, env) {
|
|
|
199
204
|
return token;
|
|
200
205
|
}
|
|
201
206
|
|
|
207
|
+
async function requireReloadToken(store, serverId, options, env) {
|
|
208
|
+
const token =
|
|
209
|
+
(await credentialFromFile(options)) ||
|
|
210
|
+
(await store.serverOwnerToken(serverId, env));
|
|
211
|
+
if (!token) {
|
|
212
|
+
throw new CliError(
|
|
213
|
+
`Reload requires the recovery owner credential for ${serverId} so the CLI can poll after SSH-derived tokens are revoked. Restore the private state file, set WARPMETAL_OWNER_TOKEN, or use --token-file.`,
|
|
214
|
+
{ exitCode: 4 },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return token;
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
function safePreparedOrder(data, stateFile) {
|
|
203
221
|
return {
|
|
204
222
|
task: data.task,
|
|
@@ -614,6 +632,98 @@ async function handleServerPower(client, store, options, context) {
|
|
|
614
632
|
return result.data.operation.state === "manual_review" ? 6 : 0;
|
|
615
633
|
}
|
|
616
634
|
|
|
635
|
+
async function applyReloadResult(store, serverId, operation) {
|
|
636
|
+
if (operation?.state !== "succeeded") return;
|
|
637
|
+
await store.invalidateServerAccess(serverId);
|
|
638
|
+
if (operation.result?.reloadImpact?.agentRuntimeAffected) {
|
|
639
|
+
await store.saveRuntime(serverId, {
|
|
640
|
+
state: "needs_reinstall",
|
|
641
|
+
desiredRevision: undefined,
|
|
642
|
+
appliedRevision: 0,
|
|
643
|
+
lastSeenAt: null,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function handleServerReload(client, store, options, context) {
|
|
649
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
650
|
+
if (stringOption(options, "confirm", { required: true }) !== "ERASE") {
|
|
651
|
+
throw new CliError("Confirm disk erasure with --confirm ERASE.", {
|
|
652
|
+
exitCode: 2,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (!booleanOption(options, "power-off-first")) {
|
|
656
|
+
throw new CliError(
|
|
657
|
+
"Authorize the guarded shutdown with --power-off-first.",
|
|
658
|
+
{ exitCode: 2 },
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const acknowledgeRuntimeReset = booleanOption(
|
|
662
|
+
options,
|
|
663
|
+
"acknowledge-agent-runtime-reset",
|
|
664
|
+
);
|
|
665
|
+
const token = await requireReloadToken(
|
|
666
|
+
store,
|
|
667
|
+
serverId,
|
|
668
|
+
options,
|
|
669
|
+
context.env,
|
|
670
|
+
);
|
|
671
|
+
const server = (await client.getServer(serverId, token)).data?.task;
|
|
672
|
+
if (server?.agentRuntime && !acknowledgeRuntimeReset) {
|
|
673
|
+
throw new CliError(
|
|
674
|
+
"Agent Runtime is enabled. Reload permanently erases sandbox workspaces, revokes supervisor identity, and requires connection-profile refresh. Confirm with --acknowledge-agent-runtime-reset.",
|
|
675
|
+
{ exitCode: 2 },
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const body = {
|
|
680
|
+
confirm: "ERASE",
|
|
681
|
+
powerOffFirst: true,
|
|
682
|
+
};
|
|
683
|
+
if (acknowledgeRuntimeReset) body.acknowledgeAgentRuntimeReset = true;
|
|
684
|
+
const hostname = stringOption(options, "hostname");
|
|
685
|
+
const osName = stringOption(options, "os");
|
|
686
|
+
const publicKeyFile = stringOption(options, "ssh-public-key-file");
|
|
687
|
+
if (hostname) body.hostname = hostname;
|
|
688
|
+
if (osName) body.osName = osName;
|
|
689
|
+
if (publicKeyFile) body.sshPublicKey = await readSshPublicKey(publicKeyFile);
|
|
690
|
+
|
|
691
|
+
const key =
|
|
692
|
+
stringOption(options, "idempotency-key") || idempotencyKey("reload");
|
|
693
|
+
let result = await client.reloadServer(serverId, body, token, key);
|
|
694
|
+
const operationId = result.data?.operation?.id;
|
|
695
|
+
if (!operationId) {
|
|
696
|
+
throw new CliError("WarpMetal did not return a reload operation ID.");
|
|
697
|
+
}
|
|
698
|
+
await store.saveOperation(operationId, serverId, "reload");
|
|
699
|
+
if (booleanOption(options, "wait")) {
|
|
700
|
+
result = await pollOperation(
|
|
701
|
+
client,
|
|
702
|
+
operationId,
|
|
703
|
+
token,
|
|
704
|
+
integerOption(options, "timeout-seconds", 900),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const operation = result.data?.operation;
|
|
709
|
+
await applyReloadResult(store, serverId, operation);
|
|
710
|
+
emit(
|
|
711
|
+
context.stdout,
|
|
712
|
+
result.data,
|
|
713
|
+
context.json,
|
|
714
|
+
`Reload operation ${operationId}: ${operation?.state}.` +
|
|
715
|
+
(operation?.state === "succeeded" &&
|
|
716
|
+
operation.result?.reloadImpact?.agentRuntimeAffected
|
|
717
|
+
? " Verify and refresh the owner SSH host key, reinstall Agent Runtime, then refresh every sandbox connection profile."
|
|
718
|
+
: operation?.state === "succeeded"
|
|
719
|
+
? " Verify and refresh the owner SSH host key before reconnecting."
|
|
720
|
+
: ""),
|
|
721
|
+
);
|
|
722
|
+
if (operation?.state === "manual_review") return 6;
|
|
723
|
+
if (operation?.state === "failed") return 5;
|
|
724
|
+
return operation?.state === "succeeded" ? 0 : 8;
|
|
725
|
+
}
|
|
726
|
+
|
|
617
727
|
async function handleOperationGet(client, store, options, context) {
|
|
618
728
|
const operationId = stringOption(options, "operation", { required: true });
|
|
619
729
|
const saved = await store.operation(operationId);
|
|
@@ -626,7 +736,10 @@ async function handleOperationGet(client, store, options, context) {
|
|
|
626
736
|
},
|
|
627
737
|
);
|
|
628
738
|
}
|
|
629
|
-
const token =
|
|
739
|
+
const token =
|
|
740
|
+
saved?.kind === "reload"
|
|
741
|
+
? await requireReloadToken(store, serverId, options, context.env)
|
|
742
|
+
: await requireServerToken(store, serverId, options, context.env);
|
|
630
743
|
const result = booleanOption(options, "wait")
|
|
631
744
|
? await pollOperation(
|
|
632
745
|
client,
|
|
@@ -635,6 +748,9 @@ async function handleOperationGet(client, store, options, context) {
|
|
|
635
748
|
integerOption(options, "timeout-seconds", 900),
|
|
636
749
|
)
|
|
637
750
|
: await client.getOperation(operationId, token);
|
|
751
|
+
if (saved?.kind === "reload") {
|
|
752
|
+
await applyReloadResult(store, serverId, result.data?.operation);
|
|
753
|
+
}
|
|
638
754
|
emit(
|
|
639
755
|
context.stdout,
|
|
640
756
|
result.data,
|
|
@@ -1076,6 +1192,69 @@ async function handleAccessGet(client, store, options, context) {
|
|
|
1076
1192
|
return result.data.accessGrant.observedState === "failed" ? 5 : 0;
|
|
1077
1193
|
}
|
|
1078
1194
|
|
|
1195
|
+
async function handleAccessRefresh(client, store, options, context) {
|
|
1196
|
+
const serverId = stringOption(options, "server", { required: true });
|
|
1197
|
+
const sandboxId = stringOption(options, "sandbox", { required: true });
|
|
1198
|
+
const grantId = stringOption(options, "grant", { required: true });
|
|
1199
|
+
const profilePath = stringOption(options, "connection-file", {
|
|
1200
|
+
required: true,
|
|
1201
|
+
});
|
|
1202
|
+
if (stringOption(options, "confirm", { required: true }) !== "REFRESH") {
|
|
1203
|
+
throw new CliError(
|
|
1204
|
+
"Confirm replacement of the pinned connection profile with --confirm REFRESH.",
|
|
1205
|
+
{ exitCode: 2 },
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
const token = await requireServerToken(store, serverId, options, context.env);
|
|
1209
|
+
const result = booleanOption(options, "wait")
|
|
1210
|
+
? await pollGrant(
|
|
1211
|
+
client,
|
|
1212
|
+
serverId,
|
|
1213
|
+
sandboxId,
|
|
1214
|
+
grantId,
|
|
1215
|
+
token,
|
|
1216
|
+
integerOption(options, "timeout-seconds", 900),
|
|
1217
|
+
(accessGrant) => accessGrant.observedState === "applied",
|
|
1218
|
+
)
|
|
1219
|
+
: await client.getAccessGrant(serverId, sandboxId, grantId, token);
|
|
1220
|
+
if (result.data?.accessGrant?.observedState !== "applied") {
|
|
1221
|
+
emit(
|
|
1222
|
+
context.stdout,
|
|
1223
|
+
{ accessGrant: result.data?.accessGrant, connectionFile: null },
|
|
1224
|
+
context.json,
|
|
1225
|
+
`Access grant ${grantId} is not applied; no profile was changed.`,
|
|
1226
|
+
);
|
|
1227
|
+
return result.data?.accessGrant?.observedState === "failed" ? 5 : 8;
|
|
1228
|
+
}
|
|
1229
|
+
const profile = connectionProfile(
|
|
1230
|
+
serverId,
|
|
1231
|
+
sandboxId,
|
|
1232
|
+
grantId,
|
|
1233
|
+
result.data.connection,
|
|
1234
|
+
);
|
|
1235
|
+
const writtenConnectionFile = await writeConnectionProfile(
|
|
1236
|
+
profilePath,
|
|
1237
|
+
profile,
|
|
1238
|
+
);
|
|
1239
|
+
await store.saveAccessGrant(
|
|
1240
|
+
serverId,
|
|
1241
|
+
result.data.accessGrant,
|
|
1242
|
+
writtenConnectionFile,
|
|
1243
|
+
);
|
|
1244
|
+
const safe = {
|
|
1245
|
+
accessGrant: result.data.accessGrant,
|
|
1246
|
+
connection: { available: true, profileWritten: true, printed: false },
|
|
1247
|
+
connectionFile: writtenConnectionFile,
|
|
1248
|
+
};
|
|
1249
|
+
emit(
|
|
1250
|
+
context.stdout,
|
|
1251
|
+
safe,
|
|
1252
|
+
context.json,
|
|
1253
|
+
`Refreshed pinned connection profile for ${grantId} at ${writtenConnectionFile}.`,
|
|
1254
|
+
);
|
|
1255
|
+
return 0;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1079
1258
|
async function handleAccessRevoke(client, store, options, context) {
|
|
1080
1259
|
const serverId = stringOption(options, "server", { required: true });
|
|
1081
1260
|
const sandboxId = stringOption(options, "sandbox", { required: true });
|
|
@@ -1254,6 +1433,22 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1254
1433
|
"timeout-seconds",
|
|
1255
1434
|
]);
|
|
1256
1435
|
return handleServerPower(client, store, options, context);
|
|
1436
|
+
case "server reload":
|
|
1437
|
+
rejectUnknownOptions(options, [
|
|
1438
|
+
...COMMON_OPTIONS,
|
|
1439
|
+
"server",
|
|
1440
|
+
"token-file",
|
|
1441
|
+
"confirm",
|
|
1442
|
+
"power-off-first",
|
|
1443
|
+
"acknowledge-agent-runtime-reset",
|
|
1444
|
+
"hostname",
|
|
1445
|
+
"os",
|
|
1446
|
+
"ssh-public-key-file",
|
|
1447
|
+
"idempotency-key",
|
|
1448
|
+
"wait",
|
|
1449
|
+
"timeout-seconds",
|
|
1450
|
+
]);
|
|
1451
|
+
return handleServerReload(client, store, options, context);
|
|
1257
1452
|
case "operation get":
|
|
1258
1453
|
rejectUnknownOptions(options, [
|
|
1259
1454
|
...COMMON_OPTIONS,
|
|
@@ -1387,6 +1582,19 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
1387
1582
|
"timeout-seconds",
|
|
1388
1583
|
]);
|
|
1389
1584
|
return handleAccessGet(client, store, options, context);
|
|
1585
|
+
case "sandbox access refresh":
|
|
1586
|
+
rejectUnknownOptions(options, [
|
|
1587
|
+
...COMMON_OPTIONS,
|
|
1588
|
+
"server",
|
|
1589
|
+
"sandbox",
|
|
1590
|
+
"grant",
|
|
1591
|
+
"token-file",
|
|
1592
|
+
"connection-file",
|
|
1593
|
+
"confirm",
|
|
1594
|
+
"wait",
|
|
1595
|
+
"timeout-seconds",
|
|
1596
|
+
]);
|
|
1597
|
+
return handleAccessRefresh(client, store, options, context);
|
|
1390
1598
|
case "sandbox access revoke":
|
|
1391
1599
|
rejectUnknownOptions(options, [
|
|
1392
1600
|
...COMMON_OPTIONS,
|
package/src/state.js
CHANGED
|
@@ -261,6 +261,20 @@ export class StateStore {
|
|
|
261
261
|
return server.ownerToken;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
async serverOwnerToken(serverId, env = process.env) {
|
|
265
|
+
if (env.WARPMETAL_OWNER_TOKEN) return env.WARPMETAL_OWNER_TOKEN;
|
|
266
|
+
return (await this.server(serverId))?.ownerToken;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async invalidateServerAccess(serverId) {
|
|
270
|
+
await this.update((state) => {
|
|
271
|
+
const server = state.servers[serverId];
|
|
272
|
+
if (!server) return;
|
|
273
|
+
delete server.accessToken;
|
|
274
|
+
delete server.accessTokenExpiresAt;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
264
278
|
async summary() {
|
|
265
279
|
const state = await this.read();
|
|
266
280
|
return {
|