@slock-ai/daemon 0.53.2 → 0.54.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/dist/chat-bridge.js +3 -5
- package/dist/{chunk-KNMCE6WB.js → chunk-VOZJ2ELH.js} +10 -1
- package/dist/{chunk-UIJF67BT.js → chunk-X366KJGT.js} +231 -68
- package/dist/cli/index.js +497 -56
- package/dist/cli/package.json +5 -0
- package/dist/core.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -19,13 +19,47 @@ var CliExit = class extends Error {
|
|
|
19
19
|
function emit(payload) {
|
|
20
20
|
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
21
21
|
}
|
|
22
|
-
function fail(code, message,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
function fail(code, message, options) {
|
|
23
|
+
const body = { ok: false, code, message };
|
|
24
|
+
if (options?.suggestedNextAction) {
|
|
25
|
+
body.suggested_next_action = options.suggestedNextAction;
|
|
26
|
+
}
|
|
27
|
+
process.stderr.write(JSON.stringify(body) + "\n");
|
|
28
|
+
throw new CliExit(options?.exitCode ?? 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/version.ts
|
|
32
|
+
import { readFileSync } from "fs";
|
|
33
|
+
var FALLBACK_VERSION = "0.0.0";
|
|
34
|
+
function readVersionFrom(candidate) {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
37
|
+
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readCliVersion(baseUrl = import.meta.url) {
|
|
43
|
+
return (
|
|
44
|
+
// Built package and daemon-bundled CLI: dist/package.json travels with dist/index.js.
|
|
45
|
+
readVersionFrom(new URL("./package.json", baseUrl)) ?? readVersionFrom(new URL("../package.json", baseUrl)) ?? FALLBACK_VERSION
|
|
46
|
+
);
|
|
25
47
|
}
|
|
26
48
|
|
|
27
49
|
// src/auth/env.ts
|
|
28
50
|
import fs from "fs";
|
|
51
|
+
import os from "os";
|
|
52
|
+
import path from "path";
|
|
53
|
+
var RAW_AGENT_ENV_KEYS = [
|
|
54
|
+
"SLOCK_AGENT_ID",
|
|
55
|
+
"SLOCK_SERVER_URL",
|
|
56
|
+
"SLOCK_SERVER_ID",
|
|
57
|
+
"SLOCK_AGENT_PROXY_URL",
|
|
58
|
+
"SLOCK_AGENT_PROXY_TOKEN",
|
|
59
|
+
"SLOCK_AGENT_PROXY_TOKEN_FILE",
|
|
60
|
+
"SLOCK_AGENT_TOKEN_FILE",
|
|
61
|
+
"SLOCK_AGENT_TOKEN"
|
|
62
|
+
];
|
|
29
63
|
var AgentBootstrapError = class extends Error {
|
|
30
64
|
constructor(code, message) {
|
|
31
65
|
super(message);
|
|
@@ -33,6 +67,47 @@ var AgentBootstrapError = class extends Error {
|
|
|
33
67
|
this.name = "AgentBootstrapError";
|
|
34
68
|
}
|
|
35
69
|
};
|
|
70
|
+
function resolveProfileDir(slug, env = process.env) {
|
|
71
|
+
if (env.SLOCK_PROFILE_DIR) {
|
|
72
|
+
return env.SLOCK_PROFILE_DIR;
|
|
73
|
+
}
|
|
74
|
+
if (env.SLOCK_HOME) {
|
|
75
|
+
return path.join(env.SLOCK_HOME, "profiles", slug);
|
|
76
|
+
}
|
|
77
|
+
const home = env.HOME ?? os.homedir();
|
|
78
|
+
return path.join(home, ".slock", "profiles", slug);
|
|
79
|
+
}
|
|
80
|
+
function resolveProfileCredentialPath(slug, env) {
|
|
81
|
+
return path.join(resolveProfileDir(slug, env), "credential.json");
|
|
82
|
+
}
|
|
83
|
+
function readProfileCredential(slug, env) {
|
|
84
|
+
const filePath = resolveProfileCredentialPath(slug, env);
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new AgentBootstrapError(
|
|
90
|
+
"PROFILE_FILE_UNREADABLE",
|
|
91
|
+
`SLOCK_PROFILE=${slug} resolved to ${filePath}, which could not be read: ${err.message}. Run \`slock agent login\` to mint a credential.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(raw);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new AgentBootstrapError(
|
|
99
|
+
"PROFILE_FILE_INVALID",
|
|
100
|
+
`${filePath} is not valid JSON: ${err.message}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.apiKey !== "string" || typeof parsed.agentId !== "string" || typeof parsed.serverUrl !== "string") {
|
|
104
|
+
throw new AgentBootstrapError(
|
|
105
|
+
"PROFILE_FILE_INVALID",
|
|
106
|
+
`${filePath} is missing required fields (apiKey, agentId, serverUrl).`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return { filePath, data: parsed };
|
|
110
|
+
}
|
|
36
111
|
function readTokenFromFile(filePath) {
|
|
37
112
|
let raw;
|
|
38
113
|
try {
|
|
@@ -53,10 +128,32 @@ function readTokenFromFile(filePath) {
|
|
|
53
128
|
return token;
|
|
54
129
|
}
|
|
55
130
|
function loadAgentContext(env = process.env) {
|
|
131
|
+
const activeCapabilities = env.SLOCK_AGENT_ACTIVE_CAPABILITIES ? env.SLOCK_AGENT_ACTIVE_CAPABILITIES.split(",").map((cap) => cap.trim()).filter(Boolean) : null;
|
|
132
|
+
const profileSlug = env.SLOCK_PROFILE;
|
|
133
|
+
if (profileSlug) {
|
|
134
|
+
const shadowed = RAW_AGENT_ENV_KEYS.filter((k) => env[k]);
|
|
135
|
+
if (shadowed.length > 0) {
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
`slock: SLOCK_PROFILE=${profileSlug} active; ignoring ${shadowed.join(", ")} from env.
|
|
138
|
+
`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const { filePath, data } = readProfileCredential(profileSlug, env);
|
|
142
|
+
return {
|
|
143
|
+
agentId: data.agentId,
|
|
144
|
+
serverUrl: data.serverUrl,
|
|
145
|
+
serverId: data.serverId ?? null,
|
|
146
|
+
token: data.apiKey,
|
|
147
|
+
clientMode: "self-hosted-runner",
|
|
148
|
+
secretSource: "profile-credential-file",
|
|
149
|
+
activeCapabilities,
|
|
150
|
+
profileSlug,
|
|
151
|
+
profileCredentialPath: filePath
|
|
152
|
+
};
|
|
153
|
+
}
|
|
56
154
|
const agentId = env.SLOCK_AGENT_ID;
|
|
57
155
|
const serverUrl = env.SLOCK_SERVER_URL;
|
|
58
156
|
const serverId = env.SLOCK_SERVER_ID ?? null;
|
|
59
|
-
const activeCapabilities = env.SLOCK_AGENT_ACTIVE_CAPABILITIES ? env.SLOCK_AGENT_ACTIVE_CAPABILITIES.split(",").map((cap) => cap.trim()).filter(Boolean) : null;
|
|
60
157
|
if (!agentId) throw new AgentBootstrapError("MISSING_AGENT_ID", "SLOCK_AGENT_ID is required");
|
|
61
158
|
if (!serverUrl) throw new AgentBootstrapError("MISSING_SERVER_URL", "SLOCK_SERVER_URL is required");
|
|
62
159
|
const agentProxyUrl = env.SLOCK_AGENT_PROXY_URL;
|
|
@@ -89,18 +186,6 @@ function loadAgentContext(env = process.env) {
|
|
|
89
186
|
activeCapabilities
|
|
90
187
|
};
|
|
91
188
|
}
|
|
92
|
-
const agentCredentialFile = env.SLOCK_AGENT_CREDENTIAL_KEY_FILE;
|
|
93
|
-
if (agentCredentialFile) {
|
|
94
|
-
return {
|
|
95
|
-
agentId,
|
|
96
|
-
serverUrl,
|
|
97
|
-
serverId,
|
|
98
|
-
token: readTokenFromFile(agentCredentialFile),
|
|
99
|
-
clientMode: "self-hosted-runner",
|
|
100
|
-
secretSource: "agent-credential-file",
|
|
101
|
-
activeCapabilities
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
189
|
const tokenFile = env.SLOCK_AGENT_TOKEN_FILE;
|
|
105
190
|
if (tokenFile) {
|
|
106
191
|
return {
|
|
@@ -127,7 +212,7 @@ function loadAgentContext(env = process.env) {
|
|
|
127
212
|
}
|
|
128
213
|
throw new AgentBootstrapError(
|
|
129
214
|
"MISSING_TOKEN",
|
|
130
|
-
"Neither SLOCK_AGENT_PROXY_TOKEN_FILE, SLOCK_AGENT_PROXY_TOKEN,
|
|
215
|
+
"Neither SLOCK_AGENT_PROXY_TOKEN_FILE, SLOCK_AGENT_PROXY_TOKEN, SLOCK_AGENT_TOKEN_FILE nor SLOCK_AGENT_TOKEN is set. The daemon should inject one of these when spawning the agent process."
|
|
131
216
|
);
|
|
132
217
|
}
|
|
133
218
|
|
|
@@ -148,12 +233,348 @@ function registerWhoamiCommand(parent) {
|
|
|
148
233
|
serverUrl: ctx.serverUrl,
|
|
149
234
|
serverId: ctx.serverId,
|
|
150
235
|
clientMode: ctx.clientMode,
|
|
151
|
-
secretSource: ctx.secretSource
|
|
236
|
+
secretSource: ctx.secretSource,
|
|
237
|
+
...ctx.profileSlug ? { profileSlug: ctx.profileSlug } : {},
|
|
238
|
+
...ctx.profileCredentialPath ? { profileCredentialPath: ctx.profileCredentialPath } : {}
|
|
152
239
|
}
|
|
153
240
|
});
|
|
154
241
|
});
|
|
155
242
|
}
|
|
156
243
|
|
|
244
|
+
// src/commands/agent/list.ts
|
|
245
|
+
import { fetch as undiciFetch } from "undici";
|
|
246
|
+
|
|
247
|
+
// src/agentLogin/deviceAuthClient.ts
|
|
248
|
+
import { fetch as fetch2 } from "undici";
|
|
249
|
+
var DeviceCodeLoginError = class extends Error {
|
|
250
|
+
constructor(code, message) {
|
|
251
|
+
super(message);
|
|
252
|
+
this.code = code;
|
|
253
|
+
this.name = "DeviceCodeLoginError";
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var ACTIONABLE_ERROR_MESSAGES = {
|
|
257
|
+
device_login_disabled: "Device login is not enabled on this Slock server. Ask an admin to set SLOCK_DEVICE_LOGIN_ENABLED=true.",
|
|
258
|
+
device_code_required: "Internal CLI bug: device_code was missing from the poll request.",
|
|
259
|
+
user_code_required: "Internal CLI bug: user_code was missing from the approve request.",
|
|
260
|
+
authorization_pending: "Still waiting for you to approve the login on the web page.",
|
|
261
|
+
expired_token: "The login code expired. Run `slock agent login` again to start a new flow.",
|
|
262
|
+
access_denied: "You denied the login request in the web approval page.",
|
|
263
|
+
device_code_consumed: "This login code has already been used. Run `slock agent login` again.",
|
|
264
|
+
device_code_invalid: "Unknown / malformed device code. Run `slock agent login` again to start a fresh flow."
|
|
265
|
+
};
|
|
266
|
+
function describeDeviceCodeLoginError(code) {
|
|
267
|
+
return ACTIONABLE_ERROR_MESSAGES[code] ?? `Device login failed (code: ${code}).`;
|
|
268
|
+
}
|
|
269
|
+
async function runDeviceCodeLogin(options) {
|
|
270
|
+
const httpFetch = options.fetchImpl ?? fetch2;
|
|
271
|
+
const base = options.serverUrl.replace(/\/+$/, "");
|
|
272
|
+
const authorizeRes = await httpFetch(`${base}/api/auth/device/authorize`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
...options.clientName ? { clientName: options.clientName } : {}
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
if (!authorizeRes.ok) {
|
|
280
|
+
const payload = await safeJson(authorizeRes);
|
|
281
|
+
throw new DeviceCodeLoginError(
|
|
282
|
+
typeof payload?.code === "string" ? payload.code : "authorize_failed",
|
|
283
|
+
describeDeviceCodeLoginError(typeof payload?.code === "string" ? payload.code : "authorize_failed")
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const authorizeBody = await authorizeRes.json();
|
|
287
|
+
if (!authorizeBody.deviceCode || !authorizeBody.userCode || !authorizeBody.verificationUri) {
|
|
288
|
+
throw new DeviceCodeLoginError(
|
|
289
|
+
"authorize_response_invalid",
|
|
290
|
+
"Server's authorize response was missing deviceCode / userCode / verificationUri."
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const verificationUri = authorizeBody.verificationUri.startsWith("http") ? authorizeBody.verificationUri : `${base}${authorizeBody.verificationUri}`;
|
|
294
|
+
await options.onUserAction({
|
|
295
|
+
verificationUri,
|
|
296
|
+
userCode: authorizeBody.userCode,
|
|
297
|
+
expiresInSeconds: authorizeBody.expiresIn ?? 0
|
|
298
|
+
});
|
|
299
|
+
const serverIntervalMs = (authorizeBody.interval ?? 5) * 1e3;
|
|
300
|
+
const pollIntervalMs = options.pollIntervalOverrideMs ?? serverIntervalMs;
|
|
301
|
+
const deadlineMs = Date.now() + Math.max(1, authorizeBody.expiresIn ?? 600) * 1e3;
|
|
302
|
+
while (Date.now() < deadlineMs) {
|
|
303
|
+
const tokenRes = await httpFetch(`${base}/api/auth/device/token`, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "content-type": "application/json" },
|
|
306
|
+
body: JSON.stringify({ deviceCode: authorizeBody.deviceCode })
|
|
307
|
+
});
|
|
308
|
+
if (tokenRes.ok) {
|
|
309
|
+
const tokenBody = await tokenRes.json();
|
|
310
|
+
if (!tokenBody.accessToken || !tokenBody.refreshToken || !tokenBody.userId) {
|
|
311
|
+
throw new DeviceCodeLoginError(
|
|
312
|
+
"token_response_invalid",
|
|
313
|
+
"Server's token response was missing accessToken / refreshToken / userId."
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
accessToken: tokenBody.accessToken,
|
|
318
|
+
refreshToken: tokenBody.refreshToken,
|
|
319
|
+
userId: tokenBody.userId
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const tokenError = await safeJson(tokenRes);
|
|
323
|
+
const code = typeof tokenError?.code === "string" ? tokenError.code : "token_failed";
|
|
324
|
+
if (code === "authorization_pending") {
|
|
325
|
+
await delay(pollIntervalMs);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
throw new DeviceCodeLoginError(code, describeDeviceCodeLoginError(code));
|
|
329
|
+
}
|
|
330
|
+
throw new DeviceCodeLoginError(
|
|
331
|
+
"expired_token",
|
|
332
|
+
describeDeviceCodeLoginError("expired_token")
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
async function safeJson(res) {
|
|
336
|
+
try {
|
|
337
|
+
return await res.json();
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function delay(ms) {
|
|
343
|
+
if (ms <= 0) return Promise.resolve();
|
|
344
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/commands/agent/list.ts
|
|
348
|
+
function describeListResult(reason, serverUrl) {
|
|
349
|
+
switch (reason) {
|
|
350
|
+
case "ok":
|
|
351
|
+
return `Ask the user which agent to bind to this machine, then run \`slock agent login --server ${serverUrl} --agent <id>\` with the selected agent id.`;
|
|
352
|
+
case "no_manageable_server":
|
|
353
|
+
return "You are logged in but don't have `manageAgents` on any server you're a member of. Ask a server owner or admin to grant the capability, then rerun `slock agent list`.";
|
|
354
|
+
case "no_agents_on_manageable_servers":
|
|
355
|
+
return "You have `manageAgents` on at least one server, but no agents exist on those servers yet. Ask the user to create an agent first (via web UI), then rerun `slock agent list`.";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function registerAgentListCommand(parent) {
|
|
359
|
+
parent.command("list").description(
|
|
360
|
+
"List Slock agents the user can mint credentials for (after a device-code login)."
|
|
361
|
+
).requiredOption("--server <url>", "Slock server base URL, e.g. https://slock.example.com").option("--client-name <label>", "Human-readable label shown on the web approval page").action(async (options) => {
|
|
362
|
+
let userSession;
|
|
363
|
+
try {
|
|
364
|
+
userSession = await runDeviceCodeLogin({
|
|
365
|
+
serverUrl: options.server,
|
|
366
|
+
...options.clientName ? { clientName: options.clientName } : {},
|
|
367
|
+
onUserAction: ({ verificationUri, userCode, expiresInSeconds }) => {
|
|
368
|
+
process.stderr.write(
|
|
369
|
+
`Open ${verificationUri} in your browser, enter code ${userCode} (expires in ~${Math.max(0, Math.floor(expiresInSeconds / 60))}m).
|
|
370
|
+
`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err instanceof DeviceCodeLoginError) {
|
|
376
|
+
fail(err.code, err.message);
|
|
377
|
+
}
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
380
|
+
const res = await undiciFetch(
|
|
381
|
+
`${options.server.replace(/\/+$/, "")}/api/agents/manageable`,
|
|
382
|
+
{
|
|
383
|
+
method: "GET",
|
|
384
|
+
headers: {
|
|
385
|
+
"content-type": "application/json",
|
|
386
|
+
authorization: `Bearer ${userSession.accessToken}`
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
let body = null;
|
|
392
|
+
try {
|
|
393
|
+
body = await res.json();
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
fail(
|
|
397
|
+
body?.code ?? `list_failed_${res.status}`,
|
|
398
|
+
body?.error ?? `Failed to list manageable agents (status ${res.status}).`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const payload = await res.json();
|
|
402
|
+
const agents = payload.data?.agents ?? [];
|
|
403
|
+
const reason = payload.data?.reason ?? (agents.length > 0 ? "ok" : "no_agents_on_manageable_servers");
|
|
404
|
+
const suggestedNextAction = describeListResult(reason, options.server);
|
|
405
|
+
emit({
|
|
406
|
+
ok: true,
|
|
407
|
+
data: {
|
|
408
|
+
agents,
|
|
409
|
+
reason,
|
|
410
|
+
...typeof payload.data?.manageable_server_count === "number" ? { manageable_server_count: payload.data.manageable_server_count } : {},
|
|
411
|
+
suggested_next_action: suggestedNextAction
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/commands/agent/login.ts
|
|
418
|
+
import { mkdir, stat, writeFile } from "fs/promises";
|
|
419
|
+
import path2 from "path";
|
|
420
|
+
import { fetch as undiciFetch2 } from "undici";
|
|
421
|
+
function registerAgentLoginCommand(parent) {
|
|
422
|
+
parent.command("login").description(
|
|
423
|
+
"Sign this CLI in as a specific Slock agent via the device-code login grant."
|
|
424
|
+
).requiredOption("--server <url>", "Slock server base URL, e.g. https://slock.example.com").requiredOption("--agent <agentId>", "Agent id to log in as").option("--client-name <label>", "Human-readable label shown on the web approval page").option("--profile-slug <slug>", "Slug to save the new profile under (defaults to the agent id). Distinct from root `slock --profile`, which selects an existing profile to use.").option("--profile-dir <path>", "Override the profile directory root (default resolution: SLOCK_HOME/profiles/<slug> when SLOCK_HOME is set, else ~/.slock/profiles/<slug>)").action(async (options) => {
|
|
425
|
+
const invalidShape = describeInvalidAgentIdShape(options.agent);
|
|
426
|
+
if (invalidShape) {
|
|
427
|
+
fail("INVALID_AGENT_ID", invalidShape, {
|
|
428
|
+
suggestedNextAction: `Run \`slock agent list --server ${options.server}\` to see valid agent ids, then rerun login with --agent <id>.`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const profileSlug = options.profileSlug ?? options.agent;
|
|
432
|
+
const profileDir = options.profileDir ?? resolveProfileDir(profileSlug);
|
|
433
|
+
const credentialPath = path2.join(profileDir, "credential.json");
|
|
434
|
+
if (await profileFileExists(credentialPath)) {
|
|
435
|
+
fail(
|
|
436
|
+
"PROFILE_ALREADY_EXISTS",
|
|
437
|
+
`Profile '${profileSlug}' already has a credential at ${credentialPath}.`,
|
|
438
|
+
{
|
|
439
|
+
suggestedNextAction: `Use a different \`--profile-slug <slug>\` to mint a coexistent credential, OR manually delete ${credentialPath} and rerun login. Note: the existing sk_agent_* on the server is NOT revoked by deleting the local file \u2014 it remains valid until it expires or is explicitly revoked from the agent settings UI.`
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
let userSession;
|
|
444
|
+
try {
|
|
445
|
+
userSession = await runDeviceCodeLogin({
|
|
446
|
+
serverUrl: options.server,
|
|
447
|
+
...options.clientName ? { clientName: options.clientName } : {},
|
|
448
|
+
onUserAction: ({ verificationUri, userCode, expiresInSeconds }) => {
|
|
449
|
+
process.stderr.write(
|
|
450
|
+
`Open ${verificationUri} in your browser, enter code ${userCode} (expires in ~${Math.max(0, Math.floor(expiresInSeconds / 60))}m).
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
if (err instanceof DeviceCodeLoginError) {
|
|
457
|
+
fail(err.code, err.message);
|
|
458
|
+
}
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
const mintRes = await undiciFetch2(
|
|
462
|
+
`${options.server.replace(/\/+$/, "")}/api/agents/${encodeURIComponent(options.agent)}/credentials`,
|
|
463
|
+
{
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: {
|
|
466
|
+
"content-type": "application/json",
|
|
467
|
+
authorization: `Bearer ${userSession.accessToken}`
|
|
468
|
+
},
|
|
469
|
+
body: JSON.stringify({})
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
if (!mintRes.ok) {
|
|
473
|
+
const body = await safeJson2(mintRes);
|
|
474
|
+
const code = body?.code ?? `mint_failed_${mintRes.status}`;
|
|
475
|
+
const detail = describeMintError(code, options.server);
|
|
476
|
+
fail(
|
|
477
|
+
code,
|
|
478
|
+
detail?.message ?? body?.error ?? `Failed to mint agent credential (status ${mintRes.status}).`,
|
|
479
|
+
detail?.suggestedNextAction ? { suggestedNextAction: detail.suggestedNextAction } : void 0
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
const minted = await mintRes.json();
|
|
483
|
+
if (!minted.apiKey || !minted.agentId || !minted.serverId) {
|
|
484
|
+
fail(
|
|
485
|
+
"mint_response_invalid",
|
|
486
|
+
"Server mint response was missing apiKey / agentId / serverId."
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
await mkdir(profileDir, { recursive: true, mode: 448 });
|
|
490
|
+
await writeFile(
|
|
491
|
+
credentialPath,
|
|
492
|
+
JSON.stringify(
|
|
493
|
+
{
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
serverUrl: options.server,
|
|
496
|
+
agentId: minted.agentId,
|
|
497
|
+
agentName: minted.agentName,
|
|
498
|
+
serverId: minted.serverId,
|
|
499
|
+
credentialId: minted.credentialId,
|
|
500
|
+
scopes: minted.scopes,
|
|
501
|
+
apiKey: minted.apiKey,
|
|
502
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
503
|
+
},
|
|
504
|
+
null,
|
|
505
|
+
2
|
|
506
|
+
) + "\n",
|
|
507
|
+
{ mode: 384 }
|
|
508
|
+
);
|
|
509
|
+
emit({
|
|
510
|
+
ok: true,
|
|
511
|
+
data: {
|
|
512
|
+
agentId: minted.agentId,
|
|
513
|
+
agentName: minted.agentName,
|
|
514
|
+
serverId: minted.serverId,
|
|
515
|
+
credentialId: minted.credentialId,
|
|
516
|
+
scopes: minted.scopes,
|
|
517
|
+
profileSlug,
|
|
518
|
+
credentialPath
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function safeJson2(res) {
|
|
524
|
+
try {
|
|
525
|
+
return await res.json();
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function profileFileExists(filePath) {
|
|
531
|
+
try {
|
|
532
|
+
await stat(filePath);
|
|
533
|
+
return true;
|
|
534
|
+
} catch {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function describeInvalidAgentIdShape(input) {
|
|
539
|
+
const trimmed = input.trim();
|
|
540
|
+
if (trimmed.length === 0) {
|
|
541
|
+
return "--agent must not be empty.";
|
|
542
|
+
}
|
|
543
|
+
if (trimmed.startsWith("@")) {
|
|
544
|
+
return "--agent expects an agent id (an opaque server-issued identifier), not an @handle.";
|
|
545
|
+
}
|
|
546
|
+
if (trimmed.startsWith("#")) {
|
|
547
|
+
return "--agent expects an agent id, not a #channel name.";
|
|
548
|
+
}
|
|
549
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.includes("/")) {
|
|
550
|
+
return "--agent expects an agent id, not a URL or path.";
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
function describeMintError(code, serverUrl) {
|
|
555
|
+
switch (code) {
|
|
556
|
+
case "device_login_disabled":
|
|
557
|
+
return { message: describeDeviceCodeLoginError(code) ?? code };
|
|
558
|
+
case "agent_missing":
|
|
559
|
+
return {
|
|
560
|
+
message: "Agent id is not known on this server, or the user you approved with isn't a member of the agent's server.",
|
|
561
|
+
suggestedNextAction: `Run \`slock agent list --server ${serverUrl}\` to see manageable agents, then rerun login with --agent <id>.`
|
|
562
|
+
};
|
|
563
|
+
case "insufficient_role":
|
|
564
|
+
return {
|
|
565
|
+
message: "The user you approved with isn't a server owner or admin on the agent's server, so they can't mint agent credentials.",
|
|
566
|
+
suggestedNextAction: "Ask a server owner or admin to grant you the `manageAgents` capability on this server, then rerun login."
|
|
567
|
+
};
|
|
568
|
+
case "scopes_invalid":
|
|
569
|
+
case "scopes_empty":
|
|
570
|
+
case "name_invalid":
|
|
571
|
+
return {
|
|
572
|
+
message: "Invalid request body for the agent credential mint. Re-run with default flags."
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
157
578
|
// ../shared/src/tracing/index.ts
|
|
158
579
|
var DEFAULT_TRACE_FLAGS = "00";
|
|
159
580
|
var TRACE_ID_HEX_LENGTH = 32;
|
|
@@ -1007,10 +1428,10 @@ function mergeDefs(...defs) {
|
|
|
1007
1428
|
function cloneDef(schema) {
|
|
1008
1429
|
return mergeDefs(schema._zod.def);
|
|
1009
1430
|
}
|
|
1010
|
-
function getElementAtPath(obj,
|
|
1011
|
-
if (!
|
|
1431
|
+
function getElementAtPath(obj, path4) {
|
|
1432
|
+
if (!path4)
|
|
1012
1433
|
return obj;
|
|
1013
|
-
return
|
|
1434
|
+
return path4.reduce((acc, key) => acc?.[key], obj);
|
|
1014
1435
|
}
|
|
1015
1436
|
function promiseAllObject(promisesObj) {
|
|
1016
1437
|
const keys = Object.keys(promisesObj);
|
|
@@ -1393,11 +1814,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1393
1814
|
}
|
|
1394
1815
|
return false;
|
|
1395
1816
|
}
|
|
1396
|
-
function prefixIssues(
|
|
1817
|
+
function prefixIssues(path4, issues) {
|
|
1397
1818
|
return issues.map((iss) => {
|
|
1398
1819
|
var _a2;
|
|
1399
1820
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
1400
|
-
iss.path.unshift(
|
|
1821
|
+
iss.path.unshift(path4);
|
|
1401
1822
|
return iss;
|
|
1402
1823
|
});
|
|
1403
1824
|
}
|
|
@@ -1580,7 +2001,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1580
2001
|
}
|
|
1581
2002
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
1582
2003
|
const result = { errors: [] };
|
|
1583
|
-
const processError = (error49,
|
|
2004
|
+
const processError = (error49, path4 = []) => {
|
|
1584
2005
|
var _a2, _b;
|
|
1585
2006
|
for (const issue2 of error49.issues) {
|
|
1586
2007
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -1590,7 +2011,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1590
2011
|
} else if (issue2.code === "invalid_element") {
|
|
1591
2012
|
processError({ issues: issue2.issues }, issue2.path);
|
|
1592
2013
|
} else {
|
|
1593
|
-
const fullpath = [...
|
|
2014
|
+
const fullpath = [...path4, ...issue2.path];
|
|
1594
2015
|
if (fullpath.length === 0) {
|
|
1595
2016
|
result.errors.push(mapper(issue2));
|
|
1596
2017
|
continue;
|
|
@@ -1622,8 +2043,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
1622
2043
|
}
|
|
1623
2044
|
function toDotPath(_path) {
|
|
1624
2045
|
const segs = [];
|
|
1625
|
-
const
|
|
1626
|
-
for (const seg of
|
|
2046
|
+
const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
2047
|
+
for (const seg of path4) {
|
|
1627
2048
|
if (typeof seg === "number")
|
|
1628
2049
|
segs.push(`[${seg}]`);
|
|
1629
2050
|
else if (typeof seg === "symbol")
|
|
@@ -13600,13 +14021,13 @@ function resolveRef(ref, ctx) {
|
|
|
13600
14021
|
if (!ref.startsWith("#")) {
|
|
13601
14022
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
13602
14023
|
}
|
|
13603
|
-
const
|
|
13604
|
-
if (
|
|
14024
|
+
const path4 = ref.slice(1).split("/").filter(Boolean);
|
|
14025
|
+
if (path4.length === 0) {
|
|
13605
14026
|
return ctx.rootSchema;
|
|
13606
14027
|
}
|
|
13607
14028
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
13608
|
-
if (
|
|
13609
|
-
const key =
|
|
14029
|
+
if (path4[0] === defsKey) {
|
|
14030
|
+
const key = path4[1];
|
|
13610
14031
|
if (!key || !ctx.defs[key]) {
|
|
13611
14032
|
throw new Error(`Reference not found: ${ref}`);
|
|
13612
14033
|
}
|
|
@@ -14038,13 +14459,16 @@ var agentCreateOperationSchema = external_exports.object({
|
|
|
14038
14459
|
name: external_exports.string().trim().min(1).max(60),
|
|
14039
14460
|
description: external_exports.string().trim().max(500).optional(),
|
|
14040
14461
|
/**
|
|
14041
|
-
*
|
|
14042
|
-
*
|
|
14043
|
-
*
|
|
14044
|
-
*
|
|
14045
|
-
*
|
|
14046
|
-
*
|
|
14462
|
+
* Optional computer placement contract. Agents may only set this when the
|
|
14463
|
+
* human request is explicitly computer-bound; server prepare resolves the
|
|
14464
|
+
* name/UUID and stores the UUID-only form. `suggestedComputer` preselects
|
|
14465
|
+
* the dialog when available; `requiredComputer` prevents silent fallback to
|
|
14466
|
+
* any other computer.
|
|
14467
|
+
*
|
|
14468
|
+
* Runtime / model / reasoning effort remain human-picked technical fields.
|
|
14047
14469
|
*/
|
|
14470
|
+
suggestedComputer: idOrHandleSchema.optional(),
|
|
14471
|
+
requiredComputer: idOrHandleSchema.optional(),
|
|
14048
14472
|
draftHint: draftHintSchema
|
|
14049
14473
|
});
|
|
14050
14474
|
var channelAddMemberOperationSchema = external_exports.object({
|
|
@@ -14067,6 +14491,11 @@ var actionCardActionSchema = external_exports.discriminatedUnion("type", [
|
|
|
14067
14491
|
channelAddMemberOperationSchema
|
|
14068
14492
|
]);
|
|
14069
14493
|
function validateActionCardAction(action) {
|
|
14494
|
+
if (action.type === "agent:create") {
|
|
14495
|
+
if (action.suggestedComputer && action.requiredComputer) {
|
|
14496
|
+
return "agent:create must include only one of suggestedComputer or requiredComputer";
|
|
14497
|
+
}
|
|
14498
|
+
}
|
|
14070
14499
|
if (action.type === "channel:add_member") {
|
|
14071
14500
|
const total = (action.humans?.length ?? 0) + (action.agents?.length ?? 0);
|
|
14072
14501
|
if (total === 0) {
|
|
@@ -15004,11 +15433,11 @@ ${opts.heldAction} Review the bounded context shown here, then choose one path.$
|
|
|
15004
15433
|
|
|
15005
15434
|
// src/commands/message/_continueDraftState.ts
|
|
15006
15435
|
import fs2 from "fs";
|
|
15007
|
-
import
|
|
15008
|
-
import
|
|
15436
|
+
import os2 from "os";
|
|
15437
|
+
import path3 from "path";
|
|
15009
15438
|
var DEFAULT_LOCAL_DRAFT_TTL_MS = 10 * 60 * 1e3;
|
|
15010
15439
|
function stateFilePath(agentId) {
|
|
15011
|
-
return
|
|
15440
|
+
return path3.join(process.env.SLOCK_CLI_DRAFT_STATE_DIR ?? os2.tmpdir(), "slock-cli-attested-send", agentId, "continue-state.json");
|
|
15012
15441
|
}
|
|
15013
15442
|
function readState(agentId) {
|
|
15014
15443
|
const filePath = stateFilePath(agentId);
|
|
@@ -15022,7 +15451,7 @@ function readState(agentId) {
|
|
|
15022
15451
|
}
|
|
15023
15452
|
function writeState(agentId, state) {
|
|
15024
15453
|
const filePath = stateFilePath(agentId);
|
|
15025
|
-
fs2.mkdirSync(
|
|
15454
|
+
fs2.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
15026
15455
|
fs2.writeFileSync(filePath, JSON.stringify(state), "utf8");
|
|
15027
15456
|
}
|
|
15028
15457
|
function getSavedDraft(agentId, target) {
|
|
@@ -15305,8 +15734,8 @@ async function drainInbox(ctx, opts) {
|
|
|
15305
15734
|
const query = [];
|
|
15306
15735
|
if (opts.block) query.push("block=true");
|
|
15307
15736
|
if (opts.block && opts.timeoutMs !== void 0) query.push(`timeout=${opts.timeoutMs}`);
|
|
15308
|
-
const
|
|
15309
|
-
const res = await client.request("GET",
|
|
15737
|
+
const path4 = query.length > 0 ? `${agentPath}/receive?${query.join("&")}` : `${agentPath}/receive`;
|
|
15738
|
+
const res = await client.request("GET", path4);
|
|
15310
15739
|
if (!res.ok) {
|
|
15311
15740
|
const code = res.status >= 500 ? "SERVER_5XX" : failCode;
|
|
15312
15741
|
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
@@ -15481,7 +15910,7 @@ function registerReactCommand(parent) {
|
|
|
15481
15910
|
}
|
|
15482
15911
|
|
|
15483
15912
|
// src/commands/attachment/upload.ts
|
|
15484
|
-
import { existsSync, statSync, readFileSync } from "fs";
|
|
15913
|
+
import { existsSync, statSync, readFileSync as readFileSync2 } from "fs";
|
|
15485
15914
|
import { basename } from "path";
|
|
15486
15915
|
var MAX_ATTACHMENT_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
15487
15916
|
var MAX_ATTACHMENT_UPLOAD_LABEL = "50MB";
|
|
@@ -15581,12 +16010,12 @@ function registerAttachmentUploadCommand(parent) {
|
|
|
15581
16010
|
if (!existsSync(opts.path)) {
|
|
15582
16011
|
fail("INVALID_ARG", `--path does not exist: ${opts.path}`);
|
|
15583
16012
|
}
|
|
15584
|
-
const
|
|
15585
|
-
if (!
|
|
16013
|
+
const stat2 = statSync(opts.path);
|
|
16014
|
+
if (!stat2.isFile()) {
|
|
15586
16015
|
fail("INVALID_ARG", `--path is not a regular file: ${opts.path}`);
|
|
15587
16016
|
}
|
|
15588
16017
|
try {
|
|
15589
|
-
validateUploadFileSize(
|
|
16018
|
+
validateUploadFileSize(stat2.size);
|
|
15590
16019
|
} catch (err) {
|
|
15591
16020
|
if (err instanceof AttachmentUploadArgError) fail(err.code, err.message);
|
|
15592
16021
|
throw err;
|
|
@@ -15609,7 +16038,7 @@ function registerAttachmentUploadCommand(parent) {
|
|
|
15609
16038
|
fail(code, resolved.error ?? `Could not resolve channel: ${opts.channel}`);
|
|
15610
16039
|
}
|
|
15611
16040
|
const channelId = resolved.data.channelId;
|
|
15612
|
-
const buffer =
|
|
16041
|
+
const buffer = readFileSync2(opts.path);
|
|
15613
16042
|
const filename = basename(opts.path);
|
|
15614
16043
|
let explicitMimeType;
|
|
15615
16044
|
try {
|
|
@@ -16011,7 +16440,7 @@ function registerProfileShowCommand(parent) {
|
|
|
16011
16440
|
|
|
16012
16441
|
// src/commands/profile/update.ts
|
|
16013
16442
|
import { basename as basename2 } from "path";
|
|
16014
|
-
import { existsSync as existsSync2, readFileSync as
|
|
16443
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
16015
16444
|
var MAX_PROFILE_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
16016
16445
|
var PROFILE_AVATAR_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
16017
16446
|
"image/jpeg",
|
|
@@ -16050,17 +16479,17 @@ function readAvatarFile(avatarFile) {
|
|
|
16050
16479
|
if (!existsSync2(avatarFile)) {
|
|
16051
16480
|
fail("PROFILE_FILE_NOT_FOUND", `Avatar file does not exist: ${avatarFile}`);
|
|
16052
16481
|
}
|
|
16053
|
-
const
|
|
16054
|
-
if (!
|
|
16482
|
+
const stat2 = statSync2(avatarFile);
|
|
16483
|
+
if (!stat2.isFile()) {
|
|
16055
16484
|
fail("PROFILE_FILE_NOT_FOUND", `Avatar file is not a regular file: ${avatarFile}`);
|
|
16056
16485
|
}
|
|
16057
|
-
if (
|
|
16486
|
+
if (stat2.size > MAX_PROFILE_AVATAR_BYTES) {
|
|
16058
16487
|
fail(
|
|
16059
16488
|
"PROFILE_AVATAR_TOO_LARGE",
|
|
16060
|
-
`Avatar file is ${
|
|
16489
|
+
`Avatar file is ${stat2.size} bytes; max size is ${MAX_PROFILE_AVATAR_BYTES} bytes`
|
|
16061
16490
|
);
|
|
16062
16491
|
}
|
|
16063
|
-
const buffer =
|
|
16492
|
+
const buffer = readFileSync3(avatarFile);
|
|
16064
16493
|
const filename = basename2(avatarFile);
|
|
16065
16494
|
const mimeType = inferImageMimeType(filename, buffer);
|
|
16066
16495
|
if (!mimeType || !PROFILE_AVATAR_MIME_TYPES.has(mimeType)) {
|
|
@@ -16683,10 +17112,22 @@ function registerReminderLogCommand(parent) {
|
|
|
16683
17112
|
// src/index.ts
|
|
16684
17113
|
var program = new Command();
|
|
16685
17114
|
program.name("slock").description(
|
|
16686
|
-
"Agent-facing
|
|
16687
|
-
).
|
|
17115
|
+
"Agent-facing CLI for Slock. Two entry shapes: (A) self-managed agent via `slock agent login --profile-slug <slug>` to create a profile, then `slock --profile <slug>` (or SLOCK_PROFILE=<slug>) to use it; (B) daemon-injected runner, where the local `slock` wrapper sets the SLOCK_AGENT_* env vars for you."
|
|
17116
|
+
).option(
|
|
17117
|
+
"-p, --profile <slug>",
|
|
17118
|
+
"Use the credential at $SLOCK_HOME/profiles/<slug>/credential.json (or ~/.slock/profiles/<slug>/credential.json when SLOCK_HOME is unset). Equivalent to setting SLOCK_PROFILE=<slug>. To create a new profile, use `slock agent login --profile-slug <slug>`."
|
|
17119
|
+
).version(readCliVersion());
|
|
17120
|
+
program.hook("preAction", () => {
|
|
17121
|
+
const opts = program.opts();
|
|
17122
|
+
if (opts.profile) {
|
|
17123
|
+
process.env.SLOCK_PROFILE = opts.profile;
|
|
17124
|
+
}
|
|
17125
|
+
});
|
|
16688
17126
|
var authCmd = program.command("auth").description("Auth introspection");
|
|
16689
17127
|
registerWhoamiCommand(authCmd);
|
|
17128
|
+
var agentCmd = program.command("agent").description("Self-managed agent onboarding (device-code login \u2192 sk_agent_* mint \u2192 ~/.slock/profiles/<slug>/credential.json)");
|
|
17129
|
+
registerAgentLoginCommand(agentCmd);
|
|
17130
|
+
registerAgentListCommand(agentCmd);
|
|
16690
17131
|
var channelCmd = program.command("channel").description("Channel membership operations");
|
|
16691
17132
|
registerChannelMembersCommand(channelCmd);
|
|
16692
17133
|
registerChannelJoinCommand(channelCmd);
|