@slock-ai/daemon 0.53.2-alpha.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -609,13 +609,16 @@ var agentCreateOperationSchema = z.object({
609
609
  name: z.string().trim().min(1).max(60),
610
610
  description: z.string().trim().max(500).optional(),
611
611
  /**
612
- * Agent can only suggest semantic intent (name + description). Technical
613
- * configuration (which computer / runtime / model / reasoning effort) is
614
- * a user prerogative the human picks those in the create dialog when
615
- * they click "Create Agent" on the card. Per stdrc 2026-05-10
616
- * #proj-approval msg=ae4ecedd: "为什么 computer、runtime 和 model 还是
617
- * 帮用户选了?" — don't let the agent prefill those.
612
+ * Optional computer placement contract. Agents may only set this when the
613
+ * human request is explicitly computer-bound; server prepare resolves the
614
+ * name/UUID and stores the UUID-only form. `suggestedComputer` preselects
615
+ * the dialog when available; `requiredComputer` prevents silent fallback to
616
+ * any other computer.
617
+ *
618
+ * Runtime / model / reasoning effort remain human-picked technical fields.
618
619
  */
620
+ suggestedComputer: idOrHandleSchema.optional(),
621
+ requiredComputer: idOrHandleSchema.optional(),
619
622
  draftHint: draftHintSchema
620
623
  });
621
624
  var channelAddMemberOperationSchema = z.object({
@@ -1942,6 +1945,9 @@ var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1942
1945
  var powershellSingleQuote = (value) => `'${value.replace(/'/g, "''")}'`;
1943
1946
  var DEFAULT_ACTIVE_CAPABILITIES = "send,read,mentions,tasks,reactions,server,channels";
1944
1947
  var safePathPart = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_");
1948
+ var RAW_CREDENTIAL_ENV_DENYLIST = [
1949
+ "SLOCK_AGENT_CREDENTIAL_KEY"
1950
+ ];
1945
1951
  var cachedOpencliBinPath;
1946
1952
  function resolveOpencliBinPath() {
1947
1953
  if (cachedOpencliBinPath !== void 0) return cachedOpencliBinPath;
@@ -2123,8 +2129,9 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(opencliBinPath)} "
2123
2129
  PATH: `${slockDir}${path2.delimiter}${process.env.PATH ?? ""}`
2124
2130
  };
2125
2131
  delete spawnEnv.SLOCK_AGENT_TOKEN;
2126
- delete spawnEnv.SLOCK_AGENT_CREDENTIAL_KEY;
2127
- delete spawnEnv.SLOCK_AGENT_CREDENTIAL_KEY_FILE;
2132
+ for (const key of RAW_CREDENTIAL_ENV_DENYLIST) {
2133
+ delete spawnEnv[key];
2134
+ }
2128
2135
  delete spawnEnv.SLOCK_AGENT_PROXY_URL;
2129
2136
  delete spawnEnv.SLOCK_AGENT_PROXY_TOKEN;
2130
2137
  delete spawnEnv.SLOCK_AGENT_PROXY_TOKEN_FILE;
@@ -2148,6 +2155,118 @@ import path3 from "path";
2148
2155
  function normalizeExecOutput(raw) {
2149
2156
  return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
2150
2157
  }
2158
+ var WINDOWS_ENVIRONMENT_SCRIPT = [
2159
+ "& {",
2160
+ " $result = [ordered]@{}",
2161
+ " foreach ($scope in @('Machine', 'User')) {",
2162
+ " $scopeEnv = [Environment]::GetEnvironmentVariables($scope)",
2163
+ " $scopeObj = [ordered]@{}",
2164
+ " foreach ($key in $scopeEnv.Keys) {",
2165
+ " $value = $scopeEnv[$key]",
2166
+ " if ($null -ne $value) { $scopeObj[$key] = [string]$value }",
2167
+ " }",
2168
+ " $result[$scope] = $scopeObj",
2169
+ " }",
2170
+ " $result | ConvertTo-Json -Compress -Depth 3",
2171
+ "}"
2172
+ ].join(" ");
2173
+ function normalizeProcessEnv(value) {
2174
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2175
+ const env = {};
2176
+ for (const [key, rawValue] of Object.entries(value)) {
2177
+ if (rawValue !== void 0 && rawValue !== null) {
2178
+ env[key] = String(rawValue);
2179
+ }
2180
+ }
2181
+ return env;
2182
+ }
2183
+ function readWindowsMachineUserEnvironment(env, execFileSyncFn) {
2184
+ try {
2185
+ const output = normalizeExecOutput(execFileSyncFn("powershell.exe", [
2186
+ "-NoProfile",
2187
+ "-NonInteractive",
2188
+ "-Command",
2189
+ WINDOWS_ENVIRONMENT_SCRIPT
2190
+ ], {
2191
+ stdio: ["ignore", "pipe", "ignore"],
2192
+ env,
2193
+ timeout: 5e3
2194
+ }));
2195
+ const parsed = JSON.parse(output || "{}");
2196
+ return {
2197
+ machine: normalizeProcessEnv(parsed.Machine),
2198
+ user: normalizeProcessEnv(parsed.User)
2199
+ };
2200
+ } catch {
2201
+ return null;
2202
+ }
2203
+ }
2204
+ function findEnvKey(env, name) {
2205
+ if (!env) return null;
2206
+ const lowerName = name.toLowerCase();
2207
+ const keys = Object.keys(env);
2208
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
2209
+ const key = keys[index];
2210
+ if (key.toLowerCase() === lowerName) return key;
2211
+ }
2212
+ return null;
2213
+ }
2214
+ function getEnvValue(env, name) {
2215
+ const key = findEnvKey(env, name);
2216
+ return key ? env?.[key] : void 0;
2217
+ }
2218
+ function setEnvValue(env, key, value) {
2219
+ const existingKey = findEnvKey(env, key);
2220
+ if (existingKey && existingKey !== key) {
2221
+ delete env[existingKey];
2222
+ }
2223
+ env[key] = value;
2224
+ }
2225
+ function mergeWindowsPathSegments(values) {
2226
+ const segments = [];
2227
+ const seen = /* @__PURE__ */ new Set();
2228
+ for (const value of values) {
2229
+ if (!value) continue;
2230
+ for (const rawSegment of value.split(";")) {
2231
+ const segment = rawSegment.trim();
2232
+ if (!segment) continue;
2233
+ const key = segment.toLowerCase();
2234
+ if (seen.has(key)) continue;
2235
+ seen.add(key);
2236
+ segments.push(segment);
2237
+ }
2238
+ }
2239
+ return segments.length > 0 ? segments.join(";") : void 0;
2240
+ }
2241
+ function mergeWindowsEnvironmentScopes(baseEnv, scopes) {
2242
+ const merged = {};
2243
+ const layers = [scopes.machine ?? {}, scopes.user ?? {}, baseEnv];
2244
+ for (const layer of layers) {
2245
+ for (const [key, value] of Object.entries(layer)) {
2246
+ if (value === void 0 || key.toLowerCase() === "path") continue;
2247
+ setEnvValue(merged, key, value);
2248
+ }
2249
+ }
2250
+ const pathKey = findEnvKey(baseEnv, "Path") ?? findEnvKey(scopes.machine, "Path") ?? findEnvKey(scopes.user, "Path") ?? "Path";
2251
+ const pathValue = mergeWindowsPathSegments([
2252
+ getEnvValue(baseEnv, "Path"),
2253
+ getEnvValue(scopes.machine, "Path"),
2254
+ getEnvValue(scopes.user, "Path")
2255
+ ]);
2256
+ if (pathValue) {
2257
+ merged[pathKey] = pathValue;
2258
+ }
2259
+ return merged;
2260
+ }
2261
+ function withWindowsUserEnvironment(env, deps = {}) {
2262
+ const platform = deps.platform ?? process.platform;
2263
+ if (platform !== "win32") return env;
2264
+ const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
2265
+ const reader = deps.windowsEnvironmentReaderFn ?? readWindowsMachineUserEnvironment;
2266
+ const scopes = reader(env, execFileSyncFn);
2267
+ if (!scopes) return env;
2268
+ return mergeWindowsEnvironmentScopes(env, scopes);
2269
+ }
2151
2270
  function resolveCommandOnWindows(command, env, execFileSyncFn, existsSyncFn) {
2152
2271
  const script = "& {$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition } }";
2153
2272
  try {
@@ -2185,7 +2304,7 @@ function resolveCommandOnWindows(command, env, execFileSyncFn, existsSyncFn) {
2185
2304
  }
2186
2305
  function resolveCommandOnPath(command, deps = {}) {
2187
2306
  const platform = deps.platform ?? process.platform;
2188
- const env = deps.env ?? process.env;
2307
+ const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
2189
2308
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
2190
2309
  const existsSyncFn = deps.existsSyncFn ?? existsSync2;
2191
2310
  if (platform === "win32") {
@@ -2211,7 +2330,7 @@ function firstExistingPath(candidates, deps = {}) {
2211
2330
  return null;
2212
2331
  }
2213
2332
  function readCommandVersion(command, args = [], deps = {}) {
2214
- const env = deps.env ?? process.env;
2333
+ const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
2215
2334
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
2216
2335
  try {
2217
2336
  const output = normalizeExecOutput(execFileSyncFn(command, [...args, "--version"], {
@@ -2488,6 +2607,7 @@ var ClaudeDriver = class {
2488
2607
  const detail = parts.join(" | ") || fallback;
2489
2608
  events.push({ kind: "error", message: detail });
2490
2609
  };
2610
+ const isProviderApiFailureText = (value, hasToolUse) => !hasToolUse && /^\s*API Error:/i.test(value) && (/\b(?:ECONNRESET|EPIPE|ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN)\b/i.test(value) || /\bUnable to connect to API\b/i.test(value) || /\b(?:timed out|timeout)\b/i.test(value) || /\b5\d{2}\b/.test(value));
2491
2611
  switch (event.type) {
2492
2612
  case "system":
2493
2613
  if (event.subtype === "init" && event.session_id) {
@@ -2503,11 +2623,16 @@ var ClaudeDriver = class {
2503
2623
  case "assistant": {
2504
2624
  const content = event.message?.content;
2505
2625
  if (Array.isArray(content)) {
2626
+ const hasToolUse = content.some((block) => block?.type === "tool_use");
2506
2627
  for (const block of content) {
2507
2628
  if (block.type === "thinking" && block.thinking) {
2508
2629
  events.push({ kind: "thinking", text: block.thinking });
2509
2630
  } else if (block.type === "text" && block.text) {
2510
- events.push({ kind: "text", text: block.text });
2631
+ if (isProviderApiFailureText(block.text, hasToolUse)) {
2632
+ events.push({ kind: "error", message: block.text });
2633
+ } else {
2634
+ events.push({ kind: "text", text: block.text });
2635
+ }
2511
2636
  } else if (block.type === "tool_use") {
2512
2637
  events.push({ kind: "tool_call", name: block.name || "unknown_tool", input: block.input });
2513
2638
  }
@@ -2821,6 +2946,11 @@ var CodexDriver = class {
2821
2946
  sessionAnnounced = false;
2822
2947
  streamedAgentMessageIds = /* @__PURE__ */ new Set();
2823
2948
  streamedReasoningIds = /* @__PURE__ */ new Set();
2949
+ /**
2950
+ * Post-tool window where the app-server may not yet accept stdin steering.
2951
+ * Gate busy-mode delivery until turn/completed or next progress.
2952
+ */
2953
+ steeringGateActive = false;
2824
2954
  async spawn(ctx) {
2825
2955
  ensureGitRepoForCodex(ctx.workingDirectory);
2826
2956
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
@@ -2835,6 +2965,7 @@ var CodexDriver = class {
2835
2965
  this.sessionAnnounced = false;
2836
2966
  this.streamedAgentMessageIds.clear();
2837
2967
  this.streamedReasoningIds.clear();
2968
+ this.steeringGateActive = false;
2838
2969
  const args = ["app-server", "--listen", "stdio://"];
2839
2970
  args.push(...this.buildRuntimeActionsConfigArgs(ctx));
2840
2971
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
@@ -2908,6 +3039,7 @@ var CodexDriver = class {
2908
3039
  if (typeof turnId === "string") {
2909
3040
  this.activeTurnId = turnId;
2910
3041
  }
3042
+ this.steeringGateActive = false;
2911
3043
  events.push({ kind: "thinking", text: "" });
2912
3044
  break;
2913
3045
  }
@@ -2918,6 +3050,7 @@ var CodexDriver = class {
2918
3050
  this.streamedAgentMessageIds.add(itemId);
2919
3051
  }
2920
3052
  if (typeof delta === "string" && delta.length > 0) {
3053
+ this.steeringGateActive = false;
2921
3054
  events.push({ kind: "text", text: delta });
2922
3055
  }
2923
3056
  break;
@@ -2930,6 +3063,7 @@ var CodexDriver = class {
2930
3063
  this.streamedReasoningIds.add(itemId);
2931
3064
  }
2932
3065
  if (typeof delta === "string" && delta.length > 0) {
3066
+ this.steeringGateActive = false;
2933
3067
  events.push({ kind: "thinking", text: delta });
2934
3068
  }
2935
3069
  break;
@@ -2946,6 +3080,7 @@ var CodexDriver = class {
2946
3080
  if (isCompleted && typeof item.id === "string" && !this.streamedReasoningIds.has(item.id)) {
2947
3081
  const text = joinReasoningText(item);
2948
3082
  if (text) {
3083
+ this.steeringGateActive = false;
2949
3084
  events.push({ kind: "thinking", text });
2950
3085
  }
2951
3086
  }
@@ -2955,6 +3090,7 @@ var CodexDriver = class {
2955
3090
  break;
2956
3091
  case "agentMessage":
2957
3092
  if (isCompleted && typeof item.id === "string" && !this.streamedAgentMessageIds.has(item.id) && typeof item.text === "string" && item.text.length > 0) {
3093
+ this.steeringGateActive = false;
2958
3094
  events.push({ kind: "text", text: item.text });
2959
3095
  }
2960
3096
  if (isCompleted && typeof item.id === "string") {
@@ -2967,6 +3103,7 @@ var CodexDriver = class {
2967
3103
  }
2968
3104
  if (isCompleted) {
2969
3105
  events.push({ kind: "tool_output", name: "shell" });
3106
+ this.steeringGateActive = true;
2970
3107
  }
2971
3108
  break;
2972
3109
  case "contextCompaction":
@@ -2996,17 +3133,24 @@ var CodexDriver = class {
2996
3133
  if (isCompleted) {
2997
3134
  const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
2998
3135
  events.push({ kind: "tool_output", name: toolName });
3136
+ this.steeringGateActive = true;
2999
3137
  }
3000
3138
  break;
3001
3139
  case "collabAgentToolCall":
3002
3140
  if (isStarted) {
3003
3141
  events.push({ kind: "tool_call", name: "collab_tool_call", input: { tool: item.tool, prompt: item.prompt } });
3004
3142
  }
3143
+ if (isCompleted) {
3144
+ this.steeringGateActive = true;
3145
+ }
3005
3146
  break;
3006
3147
  case "webSearch":
3007
3148
  if (isStarted) {
3008
3149
  events.push({ kind: "tool_call", name: "web_search", input: { query: item.query } });
3009
3150
  }
3151
+ if (isCompleted) {
3152
+ this.steeringGateActive = true;
3153
+ }
3010
3154
  break;
3011
3155
  }
3012
3156
  break;
@@ -3019,6 +3163,7 @@ var CodexDriver = class {
3019
3163
  this.activeTurnId = null;
3020
3164
  this.streamedAgentMessageIds.clear();
3021
3165
  this.streamedReasoningIds.clear();
3166
+ this.steeringGateActive = false;
3022
3167
  events.push({ kind: "turn_end", sessionId: this.threadId || void 0 });
3023
3168
  break;
3024
3169
  }
@@ -3038,7 +3183,7 @@ var CodexDriver = class {
3038
3183
  if (!this.threadId) return null;
3039
3184
  const mode = opts?.mode || "busy";
3040
3185
  if (mode === "busy") {
3041
- if (!this.activeTurnId) return null;
3186
+ if (!this.activeTurnId || this.steeringGateActive) return null;
3042
3187
  return JSON.stringify({
3043
3188
  jsonrpc: "2.0",
3044
3189
  id: this.nextRequestId(),
@@ -3452,8 +3597,9 @@ var CopilotDriver = class {
3452
3597
  import { spawn as spawn5, spawnSync } from "child_process";
3453
3598
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
3454
3599
  import path7 from "path";
3455
- async function buildCursorSpawnEnv(ctx) {
3456
- return (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
3600
+ async function buildCursorSpawnEnv(ctx, deps = {}) {
3601
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3602
+ return withWindowsUserEnvironment(spawnEnv, deps);
3457
3603
  }
3458
3604
  var CursorDriver = class {
3459
3605
  id = "cursor";
@@ -3637,9 +3783,16 @@ function detectCursorModels(runCommand = runCursorModelsCommand) {
3637
3783
  if (result.error || result.status !== 0) return null;
3638
3784
  return parseCursorModelsOutput(String(result.stdout || ""));
3639
3785
  }
3786
+ function buildCursorModelProbeEnv(deps = {}) {
3787
+ return withWindowsUserEnvironment({
3788
+ ...deps.env ?? process.env,
3789
+ FORCE_COLOR: "0",
3790
+ NO_COLOR: "1"
3791
+ }, deps);
3792
+ }
3640
3793
  function runCursorModelsCommand() {
3641
3794
  return spawnSync("cursor-agent", ["models"], {
3642
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
3795
+ env: buildCursorModelProbeEnv(),
3643
3796
  encoding: "utf8",
3644
3797
  timeout: 5e3
3645
3798
  });
@@ -4777,9 +4930,13 @@ function buildRuntimeErrorDiagnosticEnvelope(message) {
4777
4930
  const { value: excerpt, truncated } = truncateDiagnosticText(scrubbed, MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS);
4778
4931
  const httpStatus = extractHttpStatus(rawMessage);
4779
4932
  const runtimeErrorClass = classifyRuntimeError(rawMessage, httpStatus);
4933
+ const runtimeErrorReason = classifyRuntimeErrorReason(runtimeErrorClass);
4780
4934
  const runtimeErrorAction = classifyRuntimeErrorAction(rawMessage, runtimeErrorClass);
4781
4935
  const fingerprint = fingerprintRuntimeError(scrubbed);
4782
4936
  const spanAttrs = {
4937
+ turn_outcome: "failed",
4938
+ turn_subtype: "runtime_error",
4939
+ turn_reason: runtimeErrorReason,
4783
4940
  runtime_error_class: runtimeErrorClass,
4784
4941
  runtime_error_action: runtimeErrorAction,
4785
4942
  runtime_error_action_required: runtimeErrorAction !== "none",
@@ -4831,7 +4988,10 @@ function classifyRuntimeError(message, httpStatus) {
4831
4988
  return "ProviderApiError";
4832
4989
  }
4833
4990
  if (isRuntimeAuthActionRequiredText(message)) return "AuthError";
4834
- if (/\btimeout|timed out\b/i.test(message)) return "TimeoutError";
4991
+ if (/\b(?:ETIMEDOUT|timeout|timed out)\b/i.test(message)) return "TimeoutError";
4992
+ if (/\b(?:ECONNRESET|EPIPE|ECONNREFUSED|ENOTFOUND|EAI_AGAIN)\b/i.test(message) || /\bUnable to connect to API\b/i.test(message)) {
4993
+ return "ProviderConnectionError";
4994
+ }
4835
4995
  if (/stream closed before response\.completed|error decoding response body/i.test(message)) return "ProviderStreamError";
4836
4996
  if (/\brate.?limit|too many requests\b/i.test(message)) return "RateLimitError";
4837
4997
  if (/\bnot found\b/i.test(message)) return "NotFoundError";
@@ -4846,6 +5006,28 @@ function classifyRuntimeErrorAction(message, runtimeErrorClass) {
4846
5006
  function isRuntimeAuthActionRequiredText(text) {
4847
5007
  return RUNTIME_AUTH_ACTION_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
4848
5008
  }
5009
+ function classifyRuntimeErrorReason(runtimeErrorClass) {
5010
+ switch (runtimeErrorClass) {
5011
+ case "ProviderConnectionError":
5012
+ return "provider_connection_error";
5013
+ case "TimeoutError":
5014
+ return "provider_timeout";
5015
+ case "ProviderStreamError":
5016
+ return "provider_stream_error";
5017
+ case "RateLimitError":
5018
+ return "rate_limited";
5019
+ case "AuthError":
5020
+ return "auth_failed";
5021
+ case "NotFoundError":
5022
+ return "not_found";
5023
+ case "ProviderServerError":
5024
+ return "provider_server_error";
5025
+ case "ProviderApiError":
5026
+ return "provider_api_error";
5027
+ default:
5028
+ return "unclassified_runtime_error";
5029
+ }
5030
+ }
4849
5031
  function runtimeDisplayName(runtimeId) {
4850
5032
  switch (runtimeId) {
4851
5033
  case "antigravity":
@@ -5272,10 +5454,10 @@ For new channels, new agents, and adding members to an existing channel, post an
5272
5454
 
5273
5455
  - Use \`slock action prepare --target <onboarding-channel>\` and pipe an \`ActionCardAction\` JSON. Identity references are handles (\`@alice\` / \`@scout\` / \`#general\` \u2014 bare names work too), never UUIDs. Server resolves at prepare time.
5274
5456
  - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
5275
- - \`{type: "agent:create", name, description?, draftHint?}\`
5457
+ - \`{type: "agent:create", name, description?, suggestedComputer?, requiredComputer?, draftHint?}\`
5276
5458
  - \`{type: "channel:add_member", channel: "#existing-channel", humans?: ["@alice"], agents?: ["@scout"], draftHint?}\` \u2014 at least one of humans / agents must be non-empty
5277
5459
  - The owner clicks the button on the card; the matching dialog opens **prefilled with your values** (editable, deselectable for add_member). They review, adjust, and submit; the action is committed under their identity.
5278
- - Technical fields the owner must pick themselves are NOT yours to prefill on \`agent:create\`: computer, runtime, model, reasoning effort. Stay on semantic intent (name + description).
5460
+ - Runtime / model / reasoning effort are NOT yours to prefill on \`agent:create\`. If the human request explicitly binds the agent to a computer, use a structured \`requiredComputer\` (or \`suggestedComputer\` for a soft preference) instead of burying the constraint in \`draftHint\`; otherwise stay on semantic intent (name + description).
5279
5461
  - For \`channel:add_member\`, only suggest people who are actually likely candidates (already in the server, relevant to the channel's topic). The owner will deselect anyone they don't want \u2014 make their default-yes list useful, not exhaustive.
5280
5462
  - Do not just describe or list copyable specs once action cards are available \u2014 the human input cost should land at "click the card, review, submit", not "copy this name into the dialog yourself".
5281
5463
  - Do not imply the resource has been created or members added until the card flips to "Done".
@@ -5528,7 +5710,7 @@ Do not copy these answers verbatim.
5528
5710
  - When the owner agrees to a new agent or channel, **post an action card** with \`slock action prepare\`. The card lives inline in chat; the owner clicks the action button, the matching create dialog opens prefilled with your values (editable), and the resource is created under their identity when they submit.
5529
5711
  - v1 supports three action types via \`slock action prepare --target '<channel>' <<'SLOCKACTION' { ... } SLOCKACTION\`:
5530
5712
  - \`{type: "channel:create", name, visibility: "public" | "private", description?, initialHumans?: ["@alice"], initialAgents?: ["@scout"], draftHint?}\`
5531
- - \`{type: "agent:create", name, description?, draftHint?}\` \u2014 runtime / model / computer are the owner's call, not yours
5713
+ - \`{type: "agent:create", name, description?, suggestedComputer?, requiredComputer?, draftHint?}\` \u2014 runtime / model / reasoning effort are the owner's call. Use \`requiredComputer\` only when the owner explicitly says the new agent must run on that computer; use \`suggestedComputer\` for a soft preference.
5532
5714
  - \`{type: "channel:add_member", channel: "#existing-channel", humans?: ["@alice"], agents?: ["@scout"], draftHint?}\` \u2014 at least one of humans / agents must be non-empty. The owner clicks "Add Members" on the card; an AddMembers dialog opens with your suggested list (each row toggleable) and the owner submits to actually add them.
5533
5715
 
5534
5716
  - **Identity references are handles, not UUIDs.** Use \`@alice\` / \`@scout\` / \`#general\` (or bare \`alice\` / \`scout\` / \`general\`). The server resolves to UUIDs at prepare time. If a handle doesn't match a real human / agent / channel in this server you get a 422 INVALID_HANDLE error pointing at the field \u2014 fix the handle and retry. You should never see or write UUIDs in action card payloads.
@@ -5539,7 +5721,7 @@ Do not copy these answers verbatim.
5539
5721
 
5540
5722
  ### Guardrail
5541
5723
  - Do not imply you already created agents or channels unless the card state is \`executed\`.
5542
- - Do not prefill technical fields on \`agent:create\` (runtime / model / computer / reasoning effort). The owner picks those in the dialog.
5724
+ - Do not prefill runtime / model / reasoning effort on \`agent:create\`. Computer placement is only allowed as the structured \`suggestedComputer\` / \`requiredComputer\` field when the owner's request includes that placement; never rely on \`draftHint\` for a computer constraint.
5543
5725
  - If the action type the user wants is not yet supported (e.g. \`channel:add_member\`), say so plainly and offer the manual UI path; do not invent action types the schema does not accept.
5544
5726
  `;
5545
5727
  }
@@ -7065,14 +7247,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7065
7247
  });
7066
7248
  logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
7067
7249
  }
7068
- splitRuntimeProfileControlBatch(messages) {
7069
- const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
7070
- if (controlMessages.length === 0 || controlMessages.length === messages.length) {
7071
- return { nextMessages: messages, deferredMessages: [] };
7072
- }
7073
- const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
7074
- return { nextMessages: controlMessages, deferredMessages };
7075
- }
7076
7250
  containsOrdinaryInboxMessage(messages) {
7077
7251
  return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
7078
7252
  }
@@ -8602,7 +8776,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8602
8776
  ...runtimeTraceCounterAttrs(ap)
8603
8777
  });
8604
8778
  this.endRuntimeTrace(ap, "error", {
8605
- outcome: "runtime-error",
8606
8779
  ...runtimeErrorDiagnostics.spanAttrs,
8607
8780
  ...runtimeTraceCounterAttrs(ap),
8608
8781
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
@@ -8775,22 +8948,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8775
8948
  return true;
8776
8949
  }
8777
8950
  }
8778
- const split = this.splitRuntimeProfileControlBatch(messages);
8779
- if (split.deferredMessages.length > 0) {
8780
- ap.inbox.unshift(...split.deferredMessages);
8781
- ap.pendingNotificationCount += split.deferredMessages.length;
8782
- messages = split.nextMessages;
8783
- this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
8784
- agentId,
8785
- launchId: ap.launchId || void 0,
8786
- runtime: ap.config.runtime,
8787
- mode,
8788
- delivered_control_messages_count: messages.length,
8789
- deferred_messages_count: split.deferredMessages.length,
8790
- inbox_count: ap.inbox.length,
8791
- pending_notification_count: ap.pendingNotificationCount
8792
- });
8793
- }
8794
8951
  const traceAttrs = {
8795
8952
  agentId,
8796
8953
  launchId: ap.launchId || void 0,
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, exitCode = 1) {
23
- process.stderr.write(JSON.stringify({ ok: false, code, message }) + "\n");
24
- throw new CliExit(exitCode);
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, SLOCK_AGENT_CREDENTIAL_KEY_FILE, SLOCK_AGENT_TOKEN_FILE nor SLOCK_AGENT_TOKEN is set. The daemon should inject one of these when spawning the agent process."
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, path2) {
1011
- if (!path2)
1431
+ function getElementAtPath(obj, path4) {
1432
+ if (!path4)
1012
1433
  return obj;
1013
- return path2.reduce((acc, key) => acc?.[key], obj);
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(path2, issues) {
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(path2);
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, path2 = []) => {
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 = [...path2, ...issue2.path];
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 path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1626
- for (const seg of path2) {
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 path2 = ref.slice(1).split("/").filter(Boolean);
13604
- if (path2.length === 0) {
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 (path2[0] === defsKey) {
13609
- const key = path2[1];
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
- * Agent can only suggest semantic intent (name + description). Technical
14042
- * configuration (which computer / runtime / model / reasoning effort) is
14043
- * a user prerogative the human picks those in the create dialog when
14044
- * they click "Create Agent" on the card. Per stdrc 2026-05-10
14045
- * #proj-approval msg=ae4ecedd: "为什么 computer、runtime 和 model 还是
14046
- * 帮用户选了?" — don't let the agent prefill those.
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 os from "os";
15008
- import path from "path";
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 path.join(process.env.SLOCK_CLI_DRAFT_STATE_DIR ?? os.tmpdir(), "slock-cli-attested-send", agentId, "continue-state.json");
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(path.dirname(filePath), { recursive: true });
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 path2 = query.length > 0 ? `${agentPath}/receive?${query.join("&")}` : `${agentPath}/receive`;
15309
- const res = await client.request("GET", path2);
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 stat = statSync(opts.path);
15585
- if (!stat.isFile()) {
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(stat.size);
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 = readFileSync(opts.path);
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 readFileSync2, statSync as statSync2 } from "fs";
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 stat = statSync2(avatarFile);
16054
- if (!stat.isFile()) {
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 (stat.size > MAX_PROFILE_AVATAR_BYTES) {
16486
+ if (stat2.size > MAX_PROFILE_AVATAR_BYTES) {
16058
16487
  fail(
16059
16488
  "PROFILE_AVATAR_TOO_LARGE",
16060
- `Avatar file is ${stat.size} bytes; max size is ${MAX_PROFILE_AVATAR_BYTES} bytes`
16489
+ `Avatar file is ${stat2.size} bytes; max size is ${MAX_PROFILE_AVATAR_BYTES} bytes`
16061
16490
  );
16062
16491
  }
16063
- const buffer = readFileSync2(avatarFile);
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 execution interface for Slock. Invoked by daemon-spawned agent processes \u2014 not a user-facing CLI product."
16687
- ).version("0.0.1");
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);
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@slock-ai/cli",
3
+ "version": "0.0.2",
4
+ "type": "module"
5
+ }
package/dist/core.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSlockCliPath,
10
10
  resolveWorkspaceDirectoryPath,
11
11
  scanWorkspaceDirectories
12
- } from "./chunk-UIJF67BT.js";
12
+ } from "./chunk-JXS4CW3D.js";
13
13
  import {
14
14
  subscribeDaemonLogs
15
15
  } from "./chunk-KNMCE6WB.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DAEMON_CLI_USAGE,
4
4
  DaemonCore,
5
5
  parseDaemonCliArgs
6
- } from "./chunk-UIJF67BT.js";
6
+ } from "./chunk-JXS4CW3D.js";
7
7
  import "./chunk-KNMCE6WB.js";
8
8
 
9
9
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.53.2-alpha.0",
3
+ "version": "0.54.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"