@mycadet/cli 0.12.11 → 0.13.0-dev.202610171617.9ea363e
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 +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/package.json +8 -3
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +2 -2
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/nginx-ingress.test.ts +536 -23
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +6 -6
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +3 -3
- package/src/__tests__/shell-mode.test.ts +83 -0
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +10 -2
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/wake.test.ts +4 -1
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +51 -17
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/pair.ts +2 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +48 -7
- package/src/commands/upgrade.ts +78 -156
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +23 -18
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login.ts +2 -1
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +59 -22
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +17 -1
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/ingress-config.ts +31 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +1 -1
- package/src/lib/local.ts +4 -2
- package/src/lib/nginx-ingress.ts +454 -79
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +1 -0
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -1
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
package/src/commands/debug.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* `cadet debug [name]`
|
|
3
|
-
*
|
|
4
|
-
* Send Cadet staff a debug bundle of a self-hosted assistant: the
|
|
5
|
-
* workspace, its logs, and the gateway's own database, with no credentials.
|
|
6
|
-
* Staff open it on a throwaway debug clone. The cloud only hands out the
|
|
7
|
-
* upload URL while "Allow Staff Access" is on, and the bundle is deleted
|
|
8
|
-
* after seven days.
|
|
9
|
-
*
|
|
10
|
-
* Steps: the daemon is checked for the debug profile, the cloud mints
|
|
11
|
-
* the upload URL, the daemon exports with the debug profile, and this
|
|
12
|
-
* command waits for the job so the result is real.
|
|
13
|
-
*/
|
|
1
|
+
import { z } from "zod";
|
|
14
2
|
import { isCloudHome } from "../lib/cloud-values.js";
|
|
15
3
|
import {
|
|
16
4
|
formatAssistantReference,
|
|
@@ -18,13 +6,6 @@ import {
|
|
|
18
6
|
} from "../lib/assistant-config.js";
|
|
19
7
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
20
8
|
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
21
|
-
import { pollJobUntilDone } from "../lib/job-polling.js";
|
|
22
|
-
import {
|
|
23
|
-
MigrationInProgressError,
|
|
24
|
-
localRuntimeExportToGcs,
|
|
25
|
-
localRuntimeIdentity,
|
|
26
|
-
localRuntimePollJobStatus,
|
|
27
|
-
} from "../lib/local-runtime-client.js";
|
|
28
9
|
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
29
10
|
import {
|
|
30
11
|
authHeaders,
|
|
@@ -32,16 +13,7 @@ import {
|
|
|
32
13
|
getCloudUrl,
|
|
33
14
|
readCloudToken,
|
|
34
15
|
} from "../lib/cloud-client.js";
|
|
35
|
-
import { compareVersions } from "../lib/version-compat.js";
|
|
36
16
|
|
|
37
|
-
// Matches the daemon's own upload deadline (EXPORT_TO_GCS_PUT_TIMEOUT_MS in
|
|
38
|
-
// assistant/src/runtime/routes/migration-routes.ts), so a slow but healthy
|
|
39
|
-
// export is never reported as failed while it is still uploading.
|
|
40
|
-
const EXPORT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
41
|
-
// The daemon release that added `profile: "debug"`. An older daemon strips
|
|
42
|
-
// the field and exports a normal teleport bundle, which carries the owner's
|
|
43
|
-
// credentials. That must never reach the staff bucket.
|
|
44
|
-
export const DEBUG_PROFILE_MIN_VERSION = "0.12.3";
|
|
45
17
|
// Renew a cached guardian token that is about to lapse instead of starting
|
|
46
18
|
// a long export on it.
|
|
47
19
|
const TOKEN_EXPIRY_MARGIN_MS = 60 * 1000;
|
|
@@ -50,8 +22,8 @@ function printHelp(): void {
|
|
|
50
22
|
console.log(`Usage: cadet debug [name]
|
|
51
23
|
|
|
52
24
|
Send Cadet staff a debug bundle of a self-hosted assistant so they can
|
|
53
|
-
inspect
|
|
54
|
-
|
|
25
|
+
inspect its diagnostic logs and sanitized configuration. The archive
|
|
26
|
+
never holds credentials.
|
|
55
27
|
|
|
56
28
|
Arguments:
|
|
57
29
|
name Assistant ID or unique display name. Defaults to the active
|
|
@@ -61,7 +33,6 @@ Requirements:
|
|
|
61
33
|
- 'Allow Staff Access' is on for the assistant (Settings > Privacy).
|
|
62
34
|
- You are logged in ('cadet login'), which also registers the
|
|
63
35
|
assistant with the cloud.
|
|
64
|
-
- The assistant runs version ${DEBUG_PROFILE_MIN_VERSION} or newer.
|
|
65
36
|
- The assistant is self-hosted. Cadet-hosted assistants need no bundle;
|
|
66
37
|
staff clone them directly.
|
|
67
38
|
|
|
@@ -69,7 +40,7 @@ Side effects:
|
|
|
69
40
|
- Uploads the bundle to Cadet's debug-bundle storage, where it is
|
|
70
41
|
deleted after 7 days. Nothing on this machine changes.
|
|
71
42
|
- Records a 'debug bundle exported' entry in your account's audit history.
|
|
72
|
-
- Waits for the upload to finish
|
|
43
|
+
- Waits for the diagnostic upload to finish.
|
|
73
44
|
|
|
74
45
|
Examples:
|
|
75
46
|
cadet debug
|
|
@@ -80,114 +51,87 @@ Examples:
|
|
|
80
51
|
|
|
81
52
|
export const debug = Object.assign(
|
|
82
53
|
async function debug(): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const entry = resolveTargetAssistant(parseAssistantTargetArg(args));
|
|
90
|
-
const reference = formatAssistantReference(entry);
|
|
91
|
-
if (isCloudHome(entry.cloud)) {
|
|
92
|
-
console.error(
|
|
93
|
-
`${reference} is hosted by Cadet, so staff can clone it directly. No bundle is needed.`,
|
|
94
|
-
);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const cloudToken = readCloudToken();
|
|
99
|
-
if (!cloudToken) {
|
|
100
|
-
console.error("Not logged in. Run 'cadet login' first.");
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
const cloudAssistantId = entry.cloudAssistantId?.trim();
|
|
104
|
-
if (!cloudAssistantId) {
|
|
105
|
-
console.error(
|
|
106
|
-
`${reference} is not registered with the cloud yet. Run 'cadet login' and retry.`,
|
|
107
|
-
);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
const cloudUrl = entry.cloudBaseUrl?.trim() || getCloudUrl();
|
|
111
|
-
const organizationId = entry.cloudOrganizationId?.trim();
|
|
54
|
+
const args = process.argv.slice(3);
|
|
55
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
56
|
+
printHelp();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
112
59
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`${reference} runs version ${version || "unknown"}, which cannot build a debug bundle. Update it to ${DEBUG_PROFILE_MIN_VERSION} or newer ('cadet upgrade') and retry.`,
|
|
122
|
-
);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
60
|
+
const entry = resolveTargetAssistant(parseAssistantTargetArg(args));
|
|
61
|
+
const reference = formatAssistantReference(entry);
|
|
62
|
+
if (isCloudHome(entry.cloud)) {
|
|
63
|
+
console.error(
|
|
64
|
+
`${reference} is hosted by Cadet, so staff can clone it directly. No bundle is needed.`,
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
125
68
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
69
|
+
const cloudToken = readCloudToken();
|
|
70
|
+
if (!cloudToken) {
|
|
71
|
+
console.error("Not logged in. Run 'cadet login' first.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const cloudAssistantId = entry.cloudAssistantId?.trim();
|
|
75
|
+
if (!cloudAssistantId) {
|
|
76
|
+
console.error(
|
|
77
|
+
`${reference} is not registered with the cloud yet. Run 'cadet login' and retry.`,
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const cloudUrl = entry.cloudBaseUrl?.trim() || getCloudUrl();
|
|
82
|
+
const organizationId = entry.cloudOrganizationId?.trim();
|
|
83
|
+
|
|
84
|
+
const accessToken = await daemonAccessToken(entry, reference, false);
|
|
85
|
+
const headers = organizationId
|
|
86
|
+
? authHeadersForKnownOrganization(cloudToken, organizationId)
|
|
87
|
+
: await authHeaders(cloudToken, cloudUrl);
|
|
88
|
+
const urlResponse = await loopbackSafeFetch(
|
|
89
|
+
`${cloudUrl}/v1/assistants/${encodeURIComponent(cloudAssistantId)}/debug-bundle-upload-url/`,
|
|
90
|
+
{ method: "POST", headers },
|
|
145
91
|
);
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
const { url: uploadUrl } = (await urlResponse.json()) as { url: string };
|
|
149
|
-
|
|
150
|
-
// Step 3: the daemon builds and uploads the bundle.
|
|
151
|
-
let jobId: string;
|
|
152
|
-
try {
|
|
153
|
-
({ jobId } = await localRuntimeExportToGcs(entry, accessToken, {
|
|
154
|
-
uploadUrl,
|
|
155
|
-
description: "debug bundle for Cadet staff",
|
|
156
|
-
profile: "debug",
|
|
157
|
-
}));
|
|
158
|
-
} catch (err) {
|
|
159
|
-
if (err instanceof MigrationInProgressError) {
|
|
92
|
+
if (urlResponse.status === 403) {
|
|
160
93
|
console.error(
|
|
161
|
-
|
|
94
|
+
"Staff access is off for this assistant. Turn on 'Allow Staff Access' in Settings > Privacy, then retry.",
|
|
162
95
|
);
|
|
163
96
|
process.exit(1);
|
|
164
97
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
98
|
+
if (!urlResponse.ok) {
|
|
99
|
+
const detail = await urlResponse.text().catch(() => "");
|
|
100
|
+
console.error(
|
|
101
|
+
`Error: Could not get an upload URL (${urlResponse.status}): ${detail || urlResponse.statusText}`,
|
|
102
|
+
);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const { url: uploadUrl } = z
|
|
106
|
+
.object({ url: z.string().url() })
|
|
107
|
+
.parse(await urlResponse.json());
|
|
108
|
+
const logs = await loopbackSafeFetch(
|
|
109
|
+
`${entry.runtimeUrl.replace(/\/+$/, "")}/v1/logs/export`,
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
signal: AbortSignal.timeout(180_000),
|
|
113
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
if (!logs.ok) {
|
|
117
|
+
throw new Error(`Diagnostic export failed (HTTP ${logs.status}).`);
|
|
118
|
+
}
|
|
119
|
+
const upload = await fetch(uploadUrl, {
|
|
120
|
+
method: "PUT",
|
|
121
|
+
signal: AbortSignal.timeout(300_000),
|
|
122
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
123
|
+
body: await logs.blob(),
|
|
124
|
+
});
|
|
125
|
+
if (!upload.ok) {
|
|
126
|
+
throw new Error(`Diagnostic upload failed (HTTP ${upload.status}).`);
|
|
127
|
+
}
|
|
128
|
+
console.log(
|
|
129
|
+
"Debug bundle sent to Cadet. Staff can open it for the next 7 days.",
|
|
130
|
+
);
|
|
131
|
+
},
|
|
187
132
|
{ description: "Send Cadet staff a debug bundle of a self-hosted assistant" },
|
|
188
133
|
);
|
|
189
134
|
|
|
190
|
-
|
|
191
135
|
async function daemonAccessToken(
|
|
192
136
|
entry: {
|
|
193
137
|
assistantId: string;
|
package/src/commands/devices.ts
CHANGED
|
@@ -7,14 +7,11 @@
|
|
|
7
7
|
* which self-guard loopback + reject any browser/WebView Origin. The gateway
|
|
8
8
|
* only ever stores the HASHED device id, so list returns and revoke accepts the
|
|
9
9
|
* same `hashedDeviceId` (the raw device id is never persisted anywhere).
|
|
10
|
-
*
|
|
11
|
-
* This is the counterpart to `cadet unpair`: `unpair` forgets a connection on
|
|
12
|
-
* the *paired* machine (client side); `devices` revokes a device from the *host*
|
|
13
|
-
* that runs the assistant (server side).
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
import { z } from "zod";
|
|
17
13
|
import { createHash } from "node:crypto";
|
|
14
|
+
import { hostname } from "node:os";
|
|
18
15
|
|
|
19
16
|
import type { DeviceRecord } from "@mycadet/local-mode";
|
|
20
17
|
|
|
@@ -24,7 +21,11 @@ import {
|
|
|
24
21
|
getAssistantDisplayName,
|
|
25
22
|
resolveTargetAssistant,
|
|
26
23
|
} from "../lib/assistant-config";
|
|
27
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
type GuardianTokenData,
|
|
26
|
+
computeDeviceId,
|
|
27
|
+
loadGuardianToken,
|
|
28
|
+
} from "../lib/guardian-token.js";
|
|
28
29
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
29
30
|
import {
|
|
30
31
|
CLI_INTERFACE_ID,
|
|
@@ -45,7 +46,7 @@ const dockerGatewayResponseSchema = z.object({
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
function printUsage(): void {
|
|
48
|
-
console.log(`cadet devices
|
|
49
|
+
console.log(`cadet devices - List and revoke devices paired to a local assistant
|
|
49
50
|
|
|
50
51
|
USAGE:
|
|
51
52
|
cadet devices [name]
|
|
@@ -60,8 +61,7 @@ OPTIONS:
|
|
|
60
61
|
--json Print machine-readable JSON on stdout (revoke requires --yes)
|
|
61
62
|
|
|
62
63
|
Lists the devices paired to a local (host-side) assistant, or revokes one by its
|
|
63
|
-
hashed id. Runs on the machine that hosts the assistant
|
|
64
|
-
imported from another machine are managed with 'cadet unpair' instead.
|
|
64
|
+
hashed id. Runs on the machine that hosts the assistant.
|
|
65
65
|
|
|
66
66
|
EXAMPLES:
|
|
67
67
|
cadet devices
|
|
@@ -86,7 +86,6 @@ function resolveLoopbackBase(entry: AssistantEntry): string {
|
|
|
86
86
|
console.error(
|
|
87
87
|
"Run `cadet devices` on the host that runs the assistant to manage its devices.",
|
|
88
88
|
);
|
|
89
|
-
console.error("To forget this connection here, use `cadet unpair`.");
|
|
90
89
|
process.exit(1);
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -136,12 +135,38 @@ async function deviceRequest(
|
|
|
136
135
|
/**
|
|
137
136
|
* The hashed device id the gateway stores for THIS machine's own guardian
|
|
138
137
|
* credential (`leaseGuardianToken` registers it device-bound as
|
|
139
|
-
* sha256(deviceId)).
|
|
140
|
-
*
|
|
141
|
-
*
|
|
138
|
+
* sha256(deviceId)). The gateway's row hashes the deviceId the token was
|
|
139
|
+
* leased with, so prefer the stored token's deviceId and fall back to the
|
|
140
|
+
* current machine id for tokens persisted before the field was stored. It
|
|
141
|
+
* shows up in `/v1/devices` next to remotely paired devices; revoking it
|
|
142
|
+
* locks this host out of its own assistant until guardian repair.
|
|
142
143
|
*/
|
|
143
|
-
function currentHostHashedDeviceId(): string {
|
|
144
|
-
|
|
144
|
+
function currentHostHashedDeviceId(token?: GuardianTokenData | null): string {
|
|
145
|
+
const deviceId = token?.deviceId || computeDeviceId();
|
|
146
|
+
return createHash("sha256").update(deviceId).digest("hex");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The local guardian token.json IS a paired device even when the gateway has
|
|
151
|
+
* no record of it (fresh gateway DB, a lease that predates device tracking).
|
|
152
|
+
* Rebuild the row it would have — same identity the gateway would have
|
|
153
|
+
* stored — so it always shows up in `cadet devices` and stays revocable.
|
|
154
|
+
*/
|
|
155
|
+
function localHostDeviceRow(
|
|
156
|
+
token: GuardianTokenData,
|
|
157
|
+
hashedDeviceId: string,
|
|
158
|
+
): DeviceRecord {
|
|
159
|
+
const leasedAtMs = Date.parse(token.leasedAt);
|
|
160
|
+
return {
|
|
161
|
+
hashedDeviceId,
|
|
162
|
+
platform: "cli",
|
|
163
|
+
issuedAt: Number.isFinite(leasedAtMs) ? leasedAtMs : null,
|
|
164
|
+
expiresAt: null,
|
|
165
|
+
lastUsedAt: null,
|
|
166
|
+
pairingUserAgent: null,
|
|
167
|
+
clientReportedName: hostname(),
|
|
168
|
+
isCurrentHost: true,
|
|
169
|
+
};
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
/** Format an epoch-ms timestamp as ISO, or a placeholder when absent. */
|
|
@@ -180,13 +205,21 @@ async function listDevices(
|
|
|
180
205
|
}
|
|
181
206
|
|
|
182
207
|
const body = (await response.json()) as { devices?: DeviceRecord[] };
|
|
183
|
-
const
|
|
208
|
+
const token = loadGuardianToken(entry.assistantId);
|
|
209
|
+
const hostHash = currentHostHashedDeviceId(token);
|
|
184
210
|
const devices = (body.devices ?? []).map((device) =>
|
|
185
211
|
device.hashedDeviceId === hostHash
|
|
186
212
|
? { ...device, isCurrentHost: true as const }
|
|
187
213
|
: device,
|
|
188
214
|
);
|
|
189
215
|
|
|
216
|
+
// The local guardian token.json IS a paired device even when the gateway
|
|
217
|
+
// has no record of it (fresh gateway DB, lease predating device tracking):
|
|
218
|
+
// synthesize the row it would have so it stays visible and revocable here.
|
|
219
|
+
if (token && !devices.some((device) => device.hashedDeviceId === hostHash)) {
|
|
220
|
+
devices.push(localHostDeviceRow(token, hostHash));
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
if (jsonOutput) {
|
|
191
224
|
console.log(JSON.stringify({ devices }));
|
|
192
225
|
return;
|
|
@@ -250,7 +283,8 @@ async function revokeDevice(
|
|
|
250
283
|
// Warn (don't block; recoverable via guardian repair) when the target is
|
|
251
284
|
// this machine's own host credential. --json is app-driven; the UI disables
|
|
252
285
|
// the host row, so the warning stays out of automation transcripts.
|
|
253
|
-
|
|
286
|
+
const localToken = loadGuardianToken(entry.assistantId);
|
|
287
|
+
if (!jsonOutput && hashedDeviceId === currentHostHashedDeviceId(localToken)) {
|
|
254
288
|
console.error(
|
|
255
289
|
"Warning: this is this machine's own host credential. Revoking it locks this host out of the assistant until guardian repair.",
|
|
256
290
|
);
|
|
@@ -350,5 +384,5 @@ export const devices = Object.assign(
|
|
|
350
384
|
const base = resolveLoopbackBase(entry);
|
|
351
385
|
await listDevices(entry, base, jsonOutput);
|
|
352
386
|
},
|
|
353
|
-
{ description: "List or revoke devices paired to a local assistant
|
|
387
|
+
{ description: "List or revoke devices paired to a local assistant" },
|
|
354
388
|
);
|
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
import { formatGuardianRefreshCliFailure } from "@mycadet/local-mode";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
type AssistantEntry,
|
|
5
|
+
resolveTargetAssistant,
|
|
6
|
+
saveAssistantEntry,
|
|
6
7
|
} from "../../lib/assistant-config.js";
|
|
7
8
|
import {
|
|
9
|
+
GuardianLeasePermanentError,
|
|
10
|
+
leaseGuardianToken,
|
|
8
11
|
loadGuardianToken,
|
|
9
12
|
refreshGuardianTokenResult,
|
|
13
|
+
resetGuardianBootstrap,
|
|
10
14
|
} from "../../lib/guardian-token.js";
|
|
15
|
+
import { generateLocalSigningKey } from "../../lib/local.js";
|
|
11
16
|
|
|
12
17
|
function printUsage(): void {
|
|
13
|
-
console.log("Usage: cadet gateway token <subcommand>
|
|
18
|
+
console.log("Usage: cadet gateway token <subcommand> [assistantId]");
|
|
14
19
|
console.log("");
|
|
15
20
|
console.log("Manage gateway authentication tokens.");
|
|
16
21
|
console.log("");
|
|
22
|
+
console.log(
|
|
23
|
+
"Omit the assistant id to target the active (or sole) assistant.",
|
|
24
|
+
);
|
|
25
|
+
console.log("");
|
|
17
26
|
console.log("Subcommands:");
|
|
18
27
|
console.log(" get Print the current guardian access token");
|
|
19
|
-
console.log(
|
|
28
|
+
console.log(
|
|
29
|
+
" refresh Refresh an expired access token and print it (self-repairs a spent credential)",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Re-provision the guardian credential in place via the gateway's bootstrap
|
|
35
|
+
* reset — the same machinery `cadet wake --repair-guardian` runs, minus the
|
|
36
|
+
* wake. NEVER points at `cadet hatch`: hatching creates a different assistant.
|
|
37
|
+
* A fresh lease revokes other devices' paired tokens; callers say so on
|
|
38
|
+
* stderr. Returns true on success (the newly leased token is persisted).
|
|
39
|
+
*/
|
|
40
|
+
async function repairGuardianCredential(
|
|
41
|
+
entry: AssistantEntry,
|
|
42
|
+
gatewayUrl: string,
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
let bootstrapSecret = entry.guardianBootstrapSecret;
|
|
45
|
+
if (!bootstrapSecret) {
|
|
46
|
+
bootstrapSecret = generateLocalSigningKey();
|
|
47
|
+
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
48
|
+
saveAssistantEntry(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const maxAttempts = 3;
|
|
52
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
await resetGuardianBootstrap(gatewayUrl, bootstrapSecret);
|
|
55
|
+
await leaseGuardianToken(gatewayUrl, entry.assistantId, bootstrapSecret);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof GuardianLeasePermanentError) return false;
|
|
59
|
+
if (attempt < maxAttempts) {
|
|
60
|
+
await new Promise((r) => setTimeout(r, 2000 * 2 ** (attempt - 1)));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
20
65
|
}
|
|
21
66
|
|
|
22
67
|
export async function gatewayToken(): Promise<void> {
|
|
@@ -34,27 +79,15 @@ export async function gatewayToken(): Promise<void> {
|
|
|
34
79
|
process.exit(1);
|
|
35
80
|
}
|
|
36
81
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
console.error("Missing required argument: <assistantId>");
|
|
40
|
-
printUsage();
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result = lookupAssistantByIdentifier(assistantId);
|
|
45
|
-
if (result.status !== "found") {
|
|
46
|
-
console.error(formatAssistantLookupError(assistantId, result));
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
const entry = result.entry;
|
|
50
|
-
|
|
51
|
-
const tokenData = loadGuardianToken(entry.assistantId);
|
|
52
|
-
if (!tokenData) {
|
|
53
|
-
console.error("No guardian token found for this assistant.");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
82
|
+
const nameArg = args[1];
|
|
83
|
+
const entry = resolveTargetAssistant(nameArg);
|
|
56
84
|
|
|
57
85
|
if (subcommand === "get") {
|
|
86
|
+
const tokenData = loadGuardianToken(entry.assistantId);
|
|
87
|
+
if (!tokenData) {
|
|
88
|
+
console.error("No guardian token found for this assistant.");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
58
91
|
console.log(tokenData.accessToken);
|
|
59
92
|
return;
|
|
60
93
|
}
|
|
@@ -70,6 +103,27 @@ export async function gatewayToken(): Promise<void> {
|
|
|
70
103
|
entry.assistantId,
|
|
71
104
|
);
|
|
72
105
|
if (!refreshed.ok) {
|
|
106
|
+
// A spent credential (401 — expired/revoked refresh token) or a missing
|
|
107
|
+
// token file (404) is recoverable in place: re-lease via the gateway's
|
|
108
|
+
// bootstrap reset. The user explicitly invoked a refresh — this is the
|
|
109
|
+
// sanctioned recovery, never `cadet hatch` (a different assistant).
|
|
110
|
+
if (refreshed.status === 401 || refreshed.status === 404) {
|
|
111
|
+
const repaired = await repairGuardianCredential(entry, gatewayUrl);
|
|
112
|
+
if (repaired) {
|
|
113
|
+
const freshToken = loadGuardianToken(entry.assistantId);
|
|
114
|
+
if (freshToken) {
|
|
115
|
+
console.error(
|
|
116
|
+
"Re-provisioned guardian token (this revoked other devices' paired tokens).",
|
|
117
|
+
);
|
|
118
|
+
console.log(freshToken.accessToken);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.error("Guardian token could not be re-provisioned.");
|
|
123
|
+
console.error(
|
|
124
|
+
`Run \`cadet wake ${entry.assistantId} --repair-guardian\` to recover access.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
73
127
|
console.error(refreshed.error);
|
|
74
128
|
console.error(
|
|
75
129
|
formatGuardianRefreshCliFailure(refreshed.status, refreshed.error),
|
package/src/commands/pair.ts
CHANGED
|
@@ -90,7 +90,7 @@ const RETIRED_FLAGS = new Map<string, string>([
|
|
|
90
90
|
]);
|
|
91
91
|
|
|
92
92
|
function printUsage(): void {
|
|
93
|
-
console.log(`cadet pair
|
|
93
|
+
console.log(`cadet pair - Print a pairing link and QR code for another device
|
|
94
94
|
|
|
95
95
|
USAGE:
|
|
96
96
|
cadet pair [assistant] [options]
|
|
@@ -608,6 +608,6 @@ export const pair = Object.assign(
|
|
|
608
608
|
console.log("");
|
|
609
609
|
console.log(`Expires: ${challenge.expiresAt}`);
|
|
610
610
|
},
|
|
611
|
-
{ description: "Print a pairing link and QR code for another device
|
|
611
|
+
{ description: "Print a pairing link and QR code for another device" },
|
|
612
612
|
);
|
|
613
613
|
|