grokbox 0.1.0-alpha.8 → 0.1.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.
package/README.md CHANGED
@@ -10,9 +10,9 @@ This project is not affiliated with or endorsed by Anysphere, Cursor, xAI, or
10
10
  Grok Bot. Grok Bot, Cursor, and related names identify compatible products and
11
11
  remain the property of their respective owners.
12
12
 
13
- > **Alpha:** current source is `0.1.0-alpha.8`. npm `next` currently points to
14
- > `0.1.0-alpha.7`; `0.0.1` remains `latest`. Prereleases publish only after
15
- > release checks and explicitly scoped external acceptance.
13
+ > **Release:** current source is `0.1.0`. Stable versions publish to npm
14
+ > `latest`; prereleases use `next`. Upstream-private compatibility adapters
15
+ > remain experimental and subject to the documented acceptance limits.
16
16
 
17
17
  ## What it does
18
18
 
@@ -82,7 +82,7 @@ configured Profile is available, commands fail closed.
82
82
  ## Install
83
83
 
84
84
  ```bash
85
- npm install --global grokbox@next
85
+ npm install --global grokbox@latest
86
86
  grokbox --version
87
87
  gbox --help
88
88
  ```
@@ -147,7 +147,7 @@ grokbox jobs list --table
147
147
  grokbox desktop status --table
148
148
  ```
149
149
 
150
- Routine reads expose bounded metadata only: prompt bodies, webhook credentials, native paths, and unknown provider fields stay private. Box-local Bots support create/update/enable/disable/delete; create always writes a disabled definition and later mutations require explicit confirmation plus the exact revision from a fresh `agents routines list/show`. Temporal Bots are Server-authoritative: list/show, webhook credential, enable/disable, and delete use the bounded Temporal Routine bridge; current upstream contracts do not expose Temporal definition create/update, so those commands refuse instead of writing the legacy Box store. `agents routines webhook credential <agent> <routine-id> --confirm` returns the URL while hiding the key; add `--reveal-key` only when secret output is explicitly required. Without the Host bridge, Temporal Routine reads fail closed rather than reporting a false empty list. Upstream Routine writes have no native compare-and-swap or idempotency contract, so an uncertain acknowledgement is never replayed automatically.
150
+ Routine reads expose bounded metadata only: prompt bodies, webhook credentials, native paths, and unknown provider fields stay private. The same `agents routines list/show` and `agents routines webhook credential <agent> <routine-id> --confirm` commands use cloud SSOT when the selected Profile has `server.access_token_ref` (configure with `profile update <name> --server-access-token-ref file:/absolute/path/to/cloud-token`); otherwise they retain Gateway/Host behavior. Cloud keys are minted only after confirmation and reused from a scoped local cache; `--reveal-key` explicitly reveals the key, never a cloud prompt, and `--verify` never invokes the real Routine. Cloud reads are read-only; mutations retain the Host contract. Box-local create writes a disabled definition; later mutations require confirmation and a fresh revision. Temporal create/update remain unsupported. See the [Routine contract](docs/product-contract.md#711-native-routines) for setup, verification, and failure guidance.
151
151
 
152
152
  Mutating commands require explicit capability and, where destructive, explicit
153
153
  confirmation. `desktop prune run` defaults to dry-run; `--yes` invokes the
package/README.zh-CN.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  本项目与 Anysphere、Cursor、xAI 或 Grok Bot 没有隶属或背书关系。Grok Bot、Cursor 及相关名称仅用于标识兼容产品,其权利归各自所有者。
8
8
 
9
- > **Alpha:** 当前源码版本是 `0.1.0-alpha.8`。npm 的 `next` dist-tag 目前指向 `0.1.0-alpha.7`;`0.0.1` 仍是 `latest`。只有通过发布检查及明确范围的外部验收后,才会发布新的预发布版本。
9
+ > **发布:** 当前源码版本是 `0.1.0`。正式版发布到 npm `latest`,预发布版使用 `next`。上游私有接口的兼容适配器仍为实验性,遵守文档中的验收范围与限制。
10
10
 
11
11
  ## 它能做什么
12
12
 
@@ -70,7 +70,7 @@ bun run grokbox -- doctor
70
70
  ## 安装
71
71
 
72
72
  ```bash
73
- npm install --global grokbox@next
73
+ npm install --global grokbox@latest
74
74
  grokbox --version
75
75
  gbox --help
76
76
  ```
@@ -128,7 +128,7 @@ grokbox jobs list --table
128
128
  grokbox desktop status --table
129
129
  ```
130
130
 
131
- Routine 读取只输出有界元数据,不暴露 prompt 正文、Webhook credential、原生路径或未知 provider 字段。Box-local Bot 支持 create/update/enable/disable/delete;create 始终只创建 disabled definition,后续 mutation 需要显式确认,并携带刚从 `agents routines list/show` 读到的精确 revision。Temporal Bot 的 Routine 以 Server 为权威:list/show、Webhook credential、enable/disable/delete 通过受限的 Temporal Routine bridge;当前官方合同没有暴露 Temporal definition create/update,因此 CLI 会明确拒绝,而不会误写旧 Box store。`agents routines webhook credential <agent> <routine-id> --confirm` 默认返回 URL 并隐藏 key;只有显式增加 `--reveal-key` 才把 key 输出到 stdout。Host bridge 不可用时,Temporal Routine 查询 fail-closed,不再冒充为空列表。上游 Routine 写接口没有 native compare-and-swap 或 idempotency 合同,因此回执不确定时不会自动重放。
131
+ Routine 读取只输出有界元数据,不暴露 prompt 正文、Webhook credential、原生路径或未知 provider 字段。同一组 `agents routines list/show` 与 `agents routines webhook credential <agent> <routine-id> --confirm` 命令,在 selected Profile 配置 `server.access_token_ref` 时使用 cloud SSOT(通过 `profile update <name> --server-access-token-ref file:/absolute/path/to/cloud-token` 配置);未配置时保留 Gateway/Host 行为。Cloud key 仅在确认后铸造,并从按范围隔离的本地缓存复用;`--reveal-key` 显式输出 key,但不输出 cloud prompt;`--verify` 从不调用真实 Routine。Cloud 读取为只读,mutation 保留现有 Host 合同。Box-local create 创建 disabled definition,后续 mutation 需要确认与 fresh revision;Temporal create/update 仍不支持。配置、验证与失败处理见 [Routine 合同](docs/product-contract.md#711-native-routines)。
132
132
 
133
133
  变更命令需要显式 capability;破坏性操作还需要显式确认。`desktop prune run` 默认 dry-run;传入 `--yes` 后会调用上游 stop-window 路径,并删除该 fork 的 Chrome profile。
134
134
 
package/dist/index.js CHANGED
@@ -3201,9 +3201,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3201
3201
  helpWidth: context.helpWidth,
3202
3202
  outputHasColors: context.hasColors
3203
3203
  });
3204
- const text = helper.formatHelp(this, helper);
3205
- if (context.hasColors) return text;
3206
- return this._outputConfiguration.stripColor(text);
3204
+ const text2 = helper.formatHelp(this, helper);
3205
+ if (context.hasColors) return text2;
3206
+ return this._outputConfiguration.stripColor(text2);
3207
3207
  }
3208
3208
  /**
3209
3209
  * @typedef HelpContext
@@ -3363,7 +3363,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3363
3363
  * @param {(string | Function)} text - string to add, or a function returning a string
3364
3364
  * @return {Command} `this` command for chaining
3365
3365
  */
3366
- addHelpText(position, text) {
3366
+ addHelpText(position, text2) {
3367
3367
  const allowedValues = ["beforeAll", "before", "after", "afterAll"];
3368
3368
  if (!allowedValues.includes(position)) {
3369
3369
  throw new Error(`Unexpected value for position to addHelpText.
@@ -3372,10 +3372,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
3372
3372
  const helpEvent = `${position}Help`;
3373
3373
  this.on(helpEvent, (context) => {
3374
3374
  let helpStr;
3375
- if (typeof text === "function") {
3376
- helpStr = text({ error: context.error, command: context.command });
3375
+ if (typeof text2 === "function") {
3376
+ helpStr = text2({ error: context.error, command: context.command });
3377
3377
  } else {
3378
- helpStr = text;
3378
+ helpStr = text2;
3379
3379
  }
3380
3380
  if (helpStr) {
3381
3381
  context.write(`${helpStr}
@@ -3474,7 +3474,7 @@ import { fileURLToPath } from "node:url";
3474
3474
  // package.json
3475
3475
  var package_default = {
3476
3476
  name: "grokbox",
3477
- version: "0.1.0-alpha.8",
3477
+ version: "0.1.0",
3478
3478
  packageManager: "bun@1.3.14",
3479
3479
  description: "Unofficial CLI and control plane for Grok Bot cloud computers",
3480
3480
  license: "MIT",
@@ -3675,6 +3675,8 @@ var LEAF_COMMANDS = [
3675
3675
  { flags: "--keepalive-interval-ms <n>", description: "Sandbox keeper interval" },
3676
3676
  { flags: "--quota-source <source>", description: "Explicit quota source (cursor-web)" },
3677
3677
  { flags: "--quota-access-token-ref <ref>", description: "Quota-only env:/file:/keychain: token reference" },
3678
+ { flags: "--server-access-token-ref <ref>", description: "Grok Bot Server account token reference" },
3679
+ { flags: "--server-backend-url <url>", description: "Grok Bot Server backend URL" },
3678
3680
  { flags: "--secret-stdin-file <field=path>", description: "Read one secret from stdin into a protected file" }
3679
3681
  ]),
3680
3682
  stdin: "secret",
@@ -3704,6 +3706,8 @@ var LEAF_COMMANDS = [
3704
3706
  { flags: "--keepalive-interval-ms <n>", description: "Sandbox keeper interval" },
3705
3707
  { flags: "--quota-source <source>", description: "Explicit quota source (cursor-web)" },
3706
3708
  { flags: "--quota-access-token-ref <ref>", description: "Quota-only env:/file:/keychain: token reference" },
3709
+ { flags: "--server-access-token-ref <ref>", description: "Grok Bot Server account token reference" },
3710
+ { flags: "--server-backend-url <url>", description: "Grok Bot Server backend URL" },
3707
3711
  { flags: "--secret-stdin-file <field=path>", description: "Read one secret from stdin into a protected file" }
3708
3712
  ]),
3709
3713
  stdin: "secret",
@@ -3995,7 +3999,7 @@ var LEAF_COMMANDS = [
3995
3999
  {
3996
4000
  path: ["agents", "routines", "list"],
3997
4001
  usage: "grokbox agents routines list <agent> [--json|--table]",
3998
- summary: "List one agent's bounded native Routine window without prompts or credentials.",
4002
+ summary: "List one agent's cloud-backed Routine metadata without prompts or credentials.",
3999
4003
  arguments: [
4000
4004
  { syntax: "<agent>", description: "Agent ID or unambiguous name/title", role: "agent", kinds: ["agent"] }
4001
4005
  ],
@@ -4071,7 +4075,7 @@ var LEAF_COMMANDS = [
4071
4075
  },
4072
4076
  {
4073
4077
  path: ["agents", "routines", "webhook", "credential"],
4074
- usage: "grokbox agents routines webhook credential <agent> <routine-id> --confirm [--reveal-key]",
4078
+ usage: "grokbox agents routines webhook credential <agent> <routine-id> --confirm [--reveal-key] [--verify]",
4075
4079
  summary: "Read or mint one webhook Routine credential; key is hidden unless explicitly revealed.",
4076
4080
  arguments: [
4077
4081
  { syntax: "<agent>", description: "Agent ID or unambiguous name/title", role: "agent", kinds: ["agent"] },
@@ -4079,7 +4083,8 @@ var LEAF_COMMANDS = [
4079
4083
  ],
4080
4084
  options: options([
4081
4085
  { flags: "--confirm", description: "Confirm credential access/mint", required: true },
4082
- { flags: "--reveal-key", description: "Include the sensitive webhook key in stdout" }
4086
+ { flags: "--reveal-key", description: "Include the webhook key and routine prompt in stdout" },
4087
+ { flags: "--verify", description: "Check the webhook key without invoking this Routine" }
4083
4088
  ], { timeout: true }),
4084
4089
  stdin: "none",
4085
4090
  table: false,
@@ -5153,18 +5158,18 @@ function asNumber(value, fallback = 0) {
5153
5158
  function asString(value, fallback = "") {
5154
5159
  return typeof value === "string" ? value : fallback;
5155
5160
  }
5156
- function stripOneTrailingNewline(text) {
5157
- if (text.endsWith("\r\n")) return text.slice(0, -2);
5158
- if (text.endsWith("\n")) return text.slice(0, -1);
5159
- return text;
5161
+ function stripOneTrailingNewline(text2) {
5162
+ if (text2.endsWith("\r\n")) return text2.slice(0, -2);
5163
+ if (text2.endsWith("\n")) return text2.slice(0, -1);
5164
+ return text2;
5160
5165
  }
5161
5166
  var UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5162
5167
  function assertUuidV4(value, name) {
5163
5168
  if (!UUID_V4.test(value)) throw usage(`${name} must be a UUID v4.`);
5164
5169
  return value;
5165
5170
  }
5166
- function utf8Bytes(text) {
5167
- return new TextEncoder().encode(text).byteLength;
5171
+ function utf8Bytes(text2) {
5172
+ return new TextEncoder().encode(text2).byteLength;
5168
5173
  }
5169
5174
 
5170
5175
  // src/config/profile.ts
@@ -5192,10 +5197,12 @@ var PROFILE_KEYS = /* @__PURE__ */ new Set([
5192
5197
  "daemon_socket",
5193
5198
  "ssh_host",
5194
5199
  "sandbox",
5195
- "quota"
5200
+ "quota",
5201
+ "server"
5196
5202
  ]);
5197
5203
  var SANDBOX_KEYS = /* @__PURE__ */ new Set(["access_token_ref", "keepalive_interval_ms"]);
5198
5204
  var QUOTA_KEYS = /* @__PURE__ */ new Set(["source", "access_token_ref"]);
5205
+ var SERVER_KEYS = /* @__PURE__ */ new Set(["access_token_ref", "backend_url"]);
5199
5206
  var GLOBAL_KEYS = /* @__PURE__ */ new Set(["version", "current_profile"]);
5200
5207
  var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
5201
5208
  var SSH_HOST = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,254}$/;
@@ -5313,6 +5320,19 @@ function validateProfile(input) {
5313
5320
  if (accessTokenRef === void 0) throw invalid("quota.access_token_ref is required.");
5314
5321
  quota = { source, access_token_ref: accessTokenRef };
5315
5322
  }
5323
+ let server;
5324
+ if (input.server !== void 0) {
5325
+ if (!isRecord(input.server)) throw invalid("server must be an object.");
5326
+ assertKnownKeys(input.server, SERVER_KEYS, "server");
5327
+ const accessTokenRef = validateSecretRef(
5328
+ optionalString(input.server, "access_token_ref"),
5329
+ "server.access_token_ref"
5330
+ );
5331
+ if (accessTokenRef === void 0) throw invalid("server.access_token_ref is required.");
5332
+ const backendUrl = validateUrl(optionalString(input.server, "backend_url"), "server.backend_url");
5333
+ server = { access_token_ref: accessTokenRef, backend_url: backendUrl };
5334
+ if (server.backend_url === void 0) delete server.backend_url;
5335
+ }
5316
5336
  const profile = {
5317
5337
  version: 1,
5318
5338
  transport,
@@ -5328,7 +5348,8 @@ function validateProfile(input) {
5328
5348
  return value === void 0 ? void 0 : assertSshHost(value);
5329
5349
  })(),
5330
5350
  sandbox,
5331
- quota
5351
+ quota,
5352
+ server
5332
5353
  };
5333
5354
  for (const key of Object.keys(profile)) {
5334
5355
  if (profile[key] === void 0) delete profile[key];
@@ -5344,9 +5365,9 @@ function validateGlobalConfig(input) {
5344
5365
  return current === void 0 ? { version: 1 } : { version: 1, current_profile: current };
5345
5366
  }
5346
5367
  async function readJson(path, missing) {
5347
- let text;
5368
+ let text2;
5348
5369
  try {
5349
- text = await readFile2(path, "utf8");
5370
+ text2 = await readFile2(path, "utf8");
5350
5371
  } catch (error) {
5351
5372
  if (error.code === "ENOENT" && missing === "null") return null;
5352
5373
  if (error.code === "ENOENT") {
@@ -5355,7 +5376,7 @@ async function readJson(path, missing) {
5355
5376
  throw invalid("Configuration file is unreadable.");
5356
5377
  }
5357
5378
  try {
5358
- return JSON.parse(text);
5379
+ return JSON.parse(text2);
5359
5380
  } catch {
5360
5381
  throw invalid("Configuration file is not valid JSON.");
5361
5382
  }
@@ -5695,13 +5716,13 @@ function lostResponse(method) {
5695
5716
  }
5696
5717
  return new CliError("daemon_unreachable", "Daemon is unreachable.", { retryable: true });
5697
5718
  }
5698
- function parseResponse(text, status) {
5719
+ function parseResponse(text2, status) {
5699
5720
  if (status === 401 || status === 403) {
5700
5721
  throw new CliError("daemon_unauthorized", "Daemon rejected the shared credential.");
5701
5722
  }
5702
5723
  let parsed;
5703
5724
  try {
5704
- parsed = JSON.parse(text);
5725
+ parsed = JSON.parse(text2);
5705
5726
  } catch {
5706
5727
  throw new CliError("daemon_unreachable", "Daemon returned invalid JSON.");
5707
5728
  }
@@ -5795,7 +5816,7 @@ var LocalDaemonClient = class extends VersionedDaemonClient {
5795
5816
  headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) }
5796
5817
  },
5797
5818
  (res) => {
5798
- let text = "";
5819
+ let text2 = "";
5799
5820
  let bytes = 0;
5800
5821
  res.setEncoding("utf8");
5801
5822
  res.on("data", (chunk) => {
@@ -5804,12 +5825,12 @@ var LocalDaemonClient = class extends VersionedDaemonClient {
5804
5825
  req.destroy(new Error("response too large"));
5805
5826
  return;
5806
5827
  }
5807
- text += chunk;
5828
+ text2 += chunk;
5808
5829
  });
5809
5830
  res.on("end", () => {
5810
5831
  cleanup();
5811
5832
  try {
5812
- resolve5(parseResponse(text, res.statusCode ?? 0));
5833
+ resolve5(parseResponse(text2, res.statusCode ?? 0));
5813
5834
  } catch (error) {
5814
5835
  if (error instanceof CliError && error.code !== "daemon_unreachable") reject(error);
5815
5836
  else reject(lostResponse(method));
@@ -6511,10 +6532,10 @@ var GatewayClient = class {
6511
6532
  if (input.stream) {
6512
6533
  clearTimeout(timer);
6513
6534
  if (!response.ok) {
6514
- const text2 = await response.text();
6535
+ const text3 = await response.text();
6515
6536
  let parsed;
6516
6537
  try {
6517
- parsed = text2.length === 0 ? null : JSON.parse(text2);
6538
+ parsed = text3.length === 0 ? null : JSON.parse(text3);
6518
6539
  } catch {
6519
6540
  parsed = null;
6520
6541
  }
@@ -6522,9 +6543,9 @@ var GatewayClient = class {
6522
6543
  }
6523
6544
  return { status: response.status, body: null, text: "", response };
6524
6545
  }
6525
- let text = "";
6546
+ let text2 = "";
6526
6547
  try {
6527
- text = await response.text();
6548
+ text2 = await response.text();
6528
6549
  } catch {
6529
6550
  clearTimeout(timer);
6530
6551
  if (write) {
@@ -6538,14 +6559,14 @@ var GatewayClient = class {
6538
6559
  clearTimeout(timer);
6539
6560
  }
6540
6561
  let body = null;
6541
- if (text.length > 0) {
6562
+ if (text2.length > 0) {
6542
6563
  try {
6543
- body = JSON.parse(text);
6564
+ body = JSON.parse(text2);
6544
6565
  } catch {
6545
6566
  body = null;
6546
6567
  }
6547
6568
  }
6548
- return { status: response.status, body, text, response };
6569
+ return { status: response.status, body, text: text2, response };
6549
6570
  }
6550
6571
  interpret(result) {
6551
6572
  if (result.status >= 200 && result.status < 300) return result;
@@ -6666,10 +6687,10 @@ function ioFromOpts(opts) {
6666
6687
  function rejectTable(table, allowed) {
6667
6688
  if (table && !allowed) throw usage("This command does not support --table.");
6668
6689
  }
6669
- async function readPrompt(text, deps) {
6690
+ async function readPrompt(text2, deps) {
6670
6691
  let prompt;
6671
- if (text !== void 0) {
6672
- prompt = text;
6692
+ if (text2 !== void 0) {
6693
+ prompt = text2;
6673
6694
  } else if (deps.stdinIsTTY) {
6674
6695
  throw usage("Provide --text or pipe stdin. Interactive prompts are not supported.");
6675
6696
  } else {
@@ -8427,15 +8448,15 @@ function validateDaemonConfig(input) {
8427
8448
  };
8428
8449
  }
8429
8450
  async function readDaemonConfig(configDir) {
8430
- let text;
8451
+ let text2;
8431
8452
  try {
8432
- text = await readFile5(daemonConfigPath(configDir), "utf8");
8453
+ text2 = await readFile5(daemonConfigPath(configDir), "utf8");
8433
8454
  } catch (error) {
8434
8455
  if (error.code === "ENOENT") return { version: 1 };
8435
8456
  throw new CliError("profile_invalid", "Daemon config is unreadable.");
8436
8457
  }
8437
8458
  try {
8438
- return validateDaemonConfig(JSON.parse(text));
8459
+ return validateDaemonConfig(JSON.parse(text2));
8439
8460
  } catch (error) {
8440
8461
  if (error instanceof CliError) throw error;
8441
8462
  throw new CliError("profile_invalid", "Daemon config is not valid JSON.");
@@ -8733,7 +8754,7 @@ function displayFromEnviron(environ) {
8733
8754
  }
8734
8755
  function inspectDesktopProc(cmdline) {
8735
8756
  const args = cmdline.split("\0").filter((part) => part.length > 0);
8736
- const text = args.join(" ");
8757
+ const text2 = args.join(" ");
8737
8758
  let startWindow = NaN;
8738
8759
  for (const [index, arg] of args.entries()) {
8739
8760
  if (arg === "start-window" || arg.endsWith("/start-window") || arg.endsWith("/start-window.sh")) {
@@ -8745,7 +8766,7 @@ function inspectDesktopProc(cmdline) {
8745
8766
  const slash = Math.max(argv0.lastIndexOf("/"), argv0.lastIndexOf("\\"));
8746
8767
  const base = argv0.slice(slash + 1);
8747
8768
  const grok = base === "grok" || args.some((arg) => arg === "grok" || arg.endsWith("/grok"));
8748
- const taskTool = /\bTask\(/.test(text);
8769
+ const taskTool = /\bTask\(/.test(text2);
8749
8770
  return {
8750
8771
  grok,
8751
8772
  task: taskTool,
@@ -10820,15 +10841,15 @@ var GovernedFilesystem = class _GovernedFilesystem {
10820
10841
  }
10821
10842
  const sha256 = createHash5("sha256").update(content).digest("hex");
10822
10843
  try {
10823
- const text = new TextDecoder("utf-8", { fatal: true }).decode(content);
10824
- if (!text.includes("\0")) {
10844
+ const text2 = new TextDecoder("utf-8", { fatal: true }).decode(content);
10845
+ if (!text2.includes("\0")) {
10825
10846
  return {
10826
10847
  path: target.remotePath,
10827
10848
  root: target.root.name,
10828
10849
  size: content.length,
10829
10850
  sha256,
10830
10851
  encoding: "utf8",
10831
- content: text
10852
+ content: text2
10832
10853
  };
10833
10854
  }
10834
10855
  } catch {
@@ -11452,17 +11473,17 @@ async function readBody(req) {
11452
11473
  return body;
11453
11474
  }
11454
11475
  function writeResponse(res, status, body) {
11455
- const text = JSON.stringify(body);
11476
+ const text2 = JSON.stringify(body);
11456
11477
  res.writeHead(status, {
11457
11478
  "content-type": "application/json",
11458
- "content-length": Buffer.byteLength(text)
11479
+ "content-length": Buffer.byteLength(text2)
11459
11480
  });
11460
- res.end(text);
11481
+ res.end(text2);
11461
11482
  }
11462
- function parseRequest(text) {
11483
+ function parseRequest(text2) {
11463
11484
  let parsed;
11464
11485
  try {
11465
- parsed = JSON.parse(text);
11486
+ parsed = JSON.parse(text2);
11466
11487
  } catch {
11467
11488
  throw new CliError("gateway_bad_request", "Daemon request is not valid JSON.");
11468
11489
  }
@@ -12848,10 +12869,10 @@ async function queryCursorWebQuota(deps, token, timeoutMs) {
12848
12869
  { failureCode: "unexpected_status", httpStatus: response.status }
12849
12870
  );
12850
12871
  }
12851
- const text = await readBoundedText(response);
12872
+ const text2 = await readBoundedText(response);
12852
12873
  let payload;
12853
12874
  try {
12854
- payload = JSON.parse(text);
12875
+ payload = JSON.parse(text2);
12855
12876
  } catch {
12856
12877
  throw protocolFailure("response_not_json");
12857
12878
  }
@@ -13342,8 +13363,8 @@ function redactJson(value, redacted) {
13342
13363
  }
13343
13364
  async function readJsonFile(path) {
13344
13365
  try {
13345
- const text = await readFile8(path, "utf8");
13346
- return JSON.parse(text);
13366
+ const text2 = await readFile8(path, "utf8");
13367
+ return JSON.parse(text2);
13347
13368
  } catch (error) {
13348
13369
  if (error.code === "ENOENT") return void 0;
13349
13370
  if (error instanceof SyntaxError) return void 0;
@@ -13429,19 +13450,19 @@ async function copyMemoryShard(sourceRoot, destRoot, shardDir, exportPrefix, lay
13429
13450
  });
13430
13451
  }
13431
13452
  }
13432
- function tokenMatches(text, name) {
13453
+ function tokenMatches(text2, name) {
13433
13454
  if (name.length < 3) return false;
13434
13455
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13435
- return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)`).test(text);
13456
+ return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|$)`).test(text2);
13436
13457
  }
13437
- function collectWorkflowNames(text, known) {
13458
+ function collectWorkflowNames(text2, known) {
13438
13459
  const found = /* @__PURE__ */ new Set();
13439
- for (const match of text.matchAll(WORKFLOW_REF)) {
13460
+ for (const match of text2.matchAll(WORKFLOW_REF)) {
13440
13461
  const name = match[1];
13441
13462
  if (name) found.add(name);
13442
13463
  }
13443
13464
  for (const name of known) {
13444
- if (tokenMatches(text, name)) found.add(name);
13465
+ if (tokenMatches(text2, name)) found.add(name);
13445
13466
  }
13446
13467
  return [...found].sort();
13447
13468
  }
@@ -14143,18 +14164,18 @@ async function runFsWrite(deps, remotePath, raw) {
14143
14164
  const io = ioFromOpts(raw);
14144
14165
  const expectedSha256 = expectedHash(raw.expectedSha256);
14145
14166
  const client2 = await filesystemClient(deps, io.timeoutMs, FS_WRITE_CAPABILITY);
14146
- let text;
14147
- if (raw.text !== void 0) text = raw.text;
14167
+ let text2;
14168
+ if (raw.text !== void 0) text2 = raw.text;
14148
14169
  else if (deps.stdinIsTTY) throw usage("Provide --text or pipe stdin.");
14149
- else text = await deps.readStdin();
14150
- const content = Buffer.from(text, "utf8");
14170
+ else text2 = await deps.readStdin();
14171
+ const content = Buffer.from(text2, "utf8");
14151
14172
  if (content.length > FS_WRITE_MAX_BYTES) throw usage("Text write exceeds the 1 MiB limit.");
14152
14173
  const sha256 = createHash7("sha256").update(content).digest("hex");
14153
14174
  const operationId2 = deps.randomUUID();
14154
14175
  writeSuccess(deps.stdout, await mutationCall(client2, "fsWrite", {
14155
14176
  operationId: operationId2,
14156
14177
  path: remotePath,
14157
- contentUtf8: text,
14178
+ contentUtf8: text2,
14158
14179
  ...expectedSha256 ? { expectedSha256 } : {}
14159
14180
  }, remotePath, { size: content.length, sha256 }));
14160
14181
  }
@@ -14789,6 +14810,15 @@ function patchFromOptions(raw) {
14789
14810
  });
14790
14811
  }
14791
14812
  }
14813
+ if (raw.serverAccessTokenRef !== void 0 || raw.serverBackendUrl !== void 0) {
14814
+ patch.server = {};
14815
+ if (raw.serverAccessTokenRef !== void 0) {
14816
+ patch.server.access_token_ref = raw.serverAccessTokenRef;
14817
+ }
14818
+ if (raw.serverBackendUrl !== void 0) {
14819
+ patch.server.backend_url = raw.serverBackendUrl;
14820
+ }
14821
+ }
14792
14822
  const quotaSecret = raw.secretStdinFile?.startsWith("quota-access-token=") ?? false;
14793
14823
  if (raw.quotaSource !== void 0 || raw.quotaAccessTokenRef !== void 0 || quotaSecret) {
14794
14824
  if (raw.quotaSource !== "cursor-web") {
@@ -14863,7 +14893,8 @@ async function runProfileUpdate(deps, name, raw) {
14863
14893
  if (Object.keys(patch).length === 0) throw usage("profile update requires at least one connection option.");
14864
14894
  const sandbox = patch.sandbox ? { ...current?.sandbox ?? {}, ...patch.sandbox } : current?.sandbox;
14865
14895
  const quota = patch.quota ? { ...current?.quota ?? {}, ...patch.quota } : current?.quota;
14866
- const next = validateProfile({ version: 1, ...current ?? {}, ...patch, sandbox, quota });
14896
+ const server = patch.server ? { ...current?.server ?? {}, ...patch.server } : current?.server;
14897
+ const next = validateProfile({ version: 1, ...current ?? {}, ...patch, sandbox, quota, server });
14867
14898
  await persistSecretInput(deps, secretInput);
14868
14899
  await writeProfileFile(deps.configDir, name, next);
14869
14900
  writeSuccess(deps.stdout, { name, updated: true });
@@ -14882,6 +14913,7 @@ async function runProfileCapabilities(deps, name, raw) {
14882
14913
  const sandboxConfigured = Boolean(profile.sandbox?.access_token_ref);
14883
14914
  const sandboxCapability = sandboxConfigured ? "provider-authorization-dependent" : false;
14884
14915
  const quotaCapability = profile.quota?.access_token_ref ? "provider-authorization-dependent" : false;
14916
+ const serverCapability = profile.server?.access_token_ref ? "provider-authorization-dependent" : false;
14885
14917
  writeSuccess(deps.stdout, {
14886
14918
  profile: profile.name,
14887
14919
  transport: profile.transport,
@@ -14908,11 +14940,494 @@ async function runProfileCapabilities(deps, name, raw) {
14908
14940
  "sandbox.inspect": sandboxCapability,
14909
14941
  "sandbox.wake": sandboxCapability,
14910
14942
  "sandbox.keepalive": sandboxCapability,
14911
- "quota.read": quotaCapability
14943
+ "quota.read": quotaCapability,
14944
+ "grok.routine.read": serverCapability || local || directGateway || daemon,
14945
+ "grok.routine.webhook": serverCapability || local || directGateway || daemon
14912
14946
  }
14913
14947
  });
14914
14948
  }
14915
14949
 
14950
+ // src/server-routines.ts
14951
+ import { createHash as createHash8 } from "node:crypto";
14952
+ import { constants as constants4 } from "node:fs";
14953
+ import { mkdir as mkdir9, open as open6, lstat as lstat5, rename as rename6, unlink as unlink3 } from "node:fs/promises";
14954
+ import { join as join11 } from "node:path";
14955
+ var SERVER_ROUTINE_PATHS = {
14956
+ listAgents: "/aiserver.v1.GrokBotService/ListGrokBotAgents",
14957
+ listAutomations: "/aiserver.v1.GrokBotService/ListGrokBotAgentAutomations",
14958
+ mintWebhookKey: "/aiserver.v1.DashboardService/CreateAutomationWebhookApiKey"
14959
+ };
14960
+ var DEFAULT_SERVER_BACKEND_URL = "https://api2.cursor.sh";
14961
+ var MAX_RESPONSE_BYTES2 = 8 * 1024 * 1024;
14962
+ var MAX_RECORD_BYTES = 512 * 1024;
14963
+ var MAX_KEY_BYTES = 4096;
14964
+ var MAX_AGENTS = 5e3;
14965
+ var MAX_ROUTINES = 100;
14966
+ var NIL_AUTOMATION_ID = "00000000-0000-5000-8000-000000000000";
14967
+ function protocolFailure2(failureCode) {
14968
+ return new CliError(
14969
+ "capability_unavailable",
14970
+ "The Server Routine surface did not satisfy the supported bounded protocol.",
14971
+ { failureCode }
14972
+ );
14973
+ }
14974
+ function authorizationFailure2(failureCode) {
14975
+ return new CliError(
14976
+ "credential_invalid",
14977
+ "The configured Server credential was not authorized for Routine access.",
14978
+ { failureCode }
14979
+ );
14980
+ }
14981
+ function varint(value) {
14982
+ const bytes = [];
14983
+ let rest = value;
14984
+ while (rest > 127) {
14985
+ bytes.push(rest & 127 | 128);
14986
+ rest = Math.floor(rest / 128);
14987
+ }
14988
+ bytes.push(rest);
14989
+ return Buffer.from(bytes);
14990
+ }
14991
+ function lengthDelimited(field, payload) {
14992
+ return Buffer.concat([varint(field << 3 | 2), varint(payload.length), payload]);
14993
+ }
14994
+ function readVarint(buffer, offset) {
14995
+ let value = 0n;
14996
+ for (let index = 0; index < 10; index += 1) {
14997
+ const byte = buffer[offset + index];
14998
+ if (byte === void 0) throw protocolFailure2("truncated_varint");
14999
+ if (index === 9 && byte > 1) throw protocolFailure2("varint_overflow");
15000
+ value |= BigInt(byte & 127) << BigInt(index * 7);
15001
+ if ((byte & 128) === 0) return { value, next: offset + index + 1 };
15002
+ }
15003
+ throw protocolFailure2("varint_overflow");
15004
+ }
15005
+ function decodeFields(buffer) {
15006
+ const fields = [];
15007
+ let offset = 0;
15008
+ while (offset < buffer.length) {
15009
+ const key = readVarint(buffer, offset);
15010
+ offset = key.next;
15011
+ if (key.value < 8n || key.value > 0xffffffffn) throw protocolFailure2("invalid_tag");
15012
+ const number = Number(key.value >> 3n);
15013
+ const wire = Number(key.value & 7n);
15014
+ if (wire === 0) {
15015
+ const value = readVarint(buffer, offset);
15016
+ offset = value.next;
15017
+ fields.push({ number, wire, value: value.value });
15018
+ } else if (wire === 2) {
15019
+ const size = readVarint(buffer, offset);
15020
+ offset = size.next;
15021
+ if (size.value > BigInt(buffer.length - offset)) throw protocolFailure2("truncated_field");
15022
+ const length = Number(size.value);
15023
+ fields.push({ number, wire, value: buffer.subarray(offset, offset + length) });
15024
+ offset += length;
15025
+ } else if (wire === 5) {
15026
+ if (offset + 4 > buffer.length) throw protocolFailure2("truncated_field");
15027
+ fields.push({ number, wire, value: buffer.subarray(offset, offset + 4) });
15028
+ offset += 4;
15029
+ } else if (wire === 1) {
15030
+ if (offset + 8 > buffer.length) throw protocolFailure2("truncated_field");
15031
+ fields.push({ number, wire, value: buffer.subarray(offset, offset + 8) });
15032
+ offset += 8;
15033
+ } else {
15034
+ if (wire === 3) {
15035
+ offset = skipGroup(buffer, offset, number, 0);
15036
+ fields.push({ number, wire, value: 0n });
15037
+ continue;
15038
+ }
15039
+ throw protocolFailure2("unsupported_wire_type");
15040
+ }
15041
+ }
15042
+ return fields;
15043
+ }
15044
+ function skipGroup(buffer, start, expected, depth) {
15045
+ if (depth > 32) throw protocolFailure2("group_depth_exceeded");
15046
+ let offset = start;
15047
+ while (offset < buffer.length) {
15048
+ const tag = readVarint(buffer, offset);
15049
+ if (tag.value < 8n || tag.value > 0xffffffffn) throw protocolFailure2("invalid_tag");
15050
+ const number = Number(tag.value >> 3n);
15051
+ const wire = Number(tag.value & 7n);
15052
+ offset = tag.next;
15053
+ if (wire === 4) {
15054
+ if (number !== expected) throw protocolFailure2("group_mismatch");
15055
+ return offset;
15056
+ }
15057
+ if (wire === 3) offset = skipGroup(buffer, offset, number, depth + 1);
15058
+ else if (wire === 0) offset = readVarint(buffer, offset).next;
15059
+ else if (wire === 1 || wire === 5) offset += wire === 1 ? 8 : 4;
15060
+ else if (wire === 2) {
15061
+ const size = readVarint(buffer, offset);
15062
+ if (size.value > BigInt(buffer.length - size.next)) throw protocolFailure2("truncated_field");
15063
+ offset = size.next + Number(size.value);
15064
+ } else throw protocolFailure2("unsupported_wire_type");
15065
+ if (offset > buffer.length) throw protocolFailure2("truncated_field");
15066
+ }
15067
+ throw protocolFailure2("truncated_group");
15068
+ }
15069
+ function firstBytes(fields, number) {
15070
+ const hits = fields.filter((field) => field.number === number);
15071
+ if (hits.length > 1 || hits.length === 1 && hits[0].wire !== 2) {
15072
+ throw protocolFailure2("invalid_required_field");
15073
+ }
15074
+ const value = hits[0]?.value;
15075
+ return value instanceof Buffer ? value : void 0;
15076
+ }
15077
+ function text(buffer) {
15078
+ try {
15079
+ return buffer === void 0 ? "" : new TextDecoder("utf-8", { fatal: true }).decode(buffer);
15080
+ } catch {
15081
+ throw protocolFailure2("invalid_utf8");
15082
+ }
15083
+ }
15084
+ function webhookUrlFor(backendUrl, serverId) {
15085
+ return new URL(`/automations/webhook/${encodeURIComponent(serverId)}`, normalizedBackend(backendUrl)).toString();
15086
+ }
15087
+ function normalizedBackend(backendUrl) {
15088
+ let parsed;
15089
+ try {
15090
+ parsed = new URL(backendUrl);
15091
+ } catch {
15092
+ throw new CliError("profile_invalid", "Server backend URL is invalid.");
15093
+ }
15094
+ const loopback = parsed.hostname === "localhost" || parsed.hostname === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/.test(parsed.hostname);
15095
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback) || parsed.username || parsed.password || backendUrl.includes("?") || backendUrl.includes("#") || parsed.pathname !== "/") {
15096
+ throw new CliError("profile_invalid", "Server backend must be an HTTPS origin (HTTP only on loopback), without credentials, query, or fragment.");
15097
+ }
15098
+ return parsed.toString();
15099
+ }
15100
+ async function readBounded2(response, limit) {
15101
+ const declared = response.headers.get("content-length");
15102
+ if (declared !== null) {
15103
+ const bytes = Number(declared);
15104
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > limit) {
15105
+ throw protocolFailure2("response_oversized");
15106
+ }
15107
+ }
15108
+ if (!response.body) return Buffer.alloc(0);
15109
+ const reader = response.body.getReader();
15110
+ const chunks = [];
15111
+ let total = 0;
15112
+ try {
15113
+ for (; ; ) {
15114
+ const { done, value } = await reader.read();
15115
+ if (done) break;
15116
+ total += value.byteLength;
15117
+ if (total > limit) {
15118
+ await reader.cancel().catch(() => void 0);
15119
+ throw protocolFailure2("response_oversized");
15120
+ }
15121
+ chunks.push(Buffer.from(value));
15122
+ }
15123
+ } finally {
15124
+ reader.releaseLock();
15125
+ }
15126
+ return Buffer.concat(chunks);
15127
+ }
15128
+ async function call(deps, backendUrl, token, path, body, timeoutMs) {
15129
+ const controller2 = new AbortController();
15130
+ const abort2 = () => controller2.abort();
15131
+ if (deps.signal?.aborted) controller2.abort();
15132
+ else deps.signal?.addEventListener("abort", abort2, { once: true });
15133
+ const timer = setTimeout(() => controller2.abort(), timeoutMs);
15134
+ const cleanup = () => {
15135
+ clearTimeout(timer);
15136
+ deps.signal?.removeEventListener("abort", abort2);
15137
+ };
15138
+ let response;
15139
+ try {
15140
+ if (controller2.signal.aborted) throw new Error("aborted");
15141
+ response = await deps.fetch(new URL(path, normalizedBackend(backendUrl)).toString(), {
15142
+ method: "POST",
15143
+ headers: {
15144
+ accept: "application/proto",
15145
+ "content-type": "application/proto",
15146
+ authorization: `Bearer ${token}`
15147
+ },
15148
+ body: new Uint8Array(body),
15149
+ cache: "no-store",
15150
+ redirect: "manual",
15151
+ signal: controller2.signal
15152
+ });
15153
+ } catch {
15154
+ cleanup();
15155
+ throw new CliError(
15156
+ "gateway_unreachable",
15157
+ controller2.signal.aborted ? "The Server Routine request timed out." : "The Server Routine endpoint is unreachable.",
15158
+ { failureCode: controller2.signal.aborted ? "request_timeout" : "network_failure", retryable: true }
15159
+ );
15160
+ }
15161
+ try {
15162
+ if (controller2.signal.aborted) {
15163
+ await response.body?.cancel().catch(() => void 0);
15164
+ throw protocolFailure2("request_cancelled");
15165
+ }
15166
+ if (response.redirected || response.status >= 300 && response.status < 400) {
15167
+ await response.body?.cancel().catch(() => void 0);
15168
+ throw protocolFailure2("redirect_refused");
15169
+ }
15170
+ if (response.status === 401 || response.status === 403) {
15171
+ const detail = await readBounded2(response, MAX_RESPONSE_BYTES2).then((buffer) => buffer.toString("utf8")).catch(() => "");
15172
+ throw authorizationFailure2(authorizationFailureCode(detail));
15173
+ }
15174
+ if (response.status === 404) {
15175
+ await response.body?.cancel().catch(() => void 0);
15176
+ throw new CliError("target_not_found", "The Server does not know that Routine.", {
15177
+ failureCode: "routine_not_found",
15178
+ httpStatus: 404
15179
+ });
15180
+ }
15181
+ if (response.status === 408 || response.status === 429 || response.status >= 500) {
15182
+ await response.body?.cancel().catch(() => void 0);
15183
+ throw new CliError("gateway_unreachable", "The Server Routine endpoint refused the request.", {
15184
+ failureCode: "provider_refused",
15185
+ httpStatus: response.status,
15186
+ retryable: true
15187
+ });
15188
+ }
15189
+ if (response.status !== 200) {
15190
+ await response.body?.cancel().catch(() => void 0);
15191
+ throw new CliError(
15192
+ "capability_unavailable",
15193
+ "The Server Routine method is unavailable for this credential.",
15194
+ { failureCode: "unexpected_status", httpStatus: response.status }
15195
+ );
15196
+ }
15197
+ if (response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/proto") {
15198
+ await response.body?.cancel().catch(() => void 0);
15199
+ throw protocolFailure2("response_content_type_invalid");
15200
+ }
15201
+ return await readBounded2(response, MAX_RESPONSE_BYTES2);
15202
+ } catch (error) {
15203
+ if (error instanceof CliError) throw error;
15204
+ throw new CliError(
15205
+ "gateway_unreachable",
15206
+ controller2.signal.aborted ? "The Server Routine request timed out." : "The Server Routine response could not be read.",
15207
+ { failureCode: controller2.signal.aborted ? "request_timeout" : "response_unreadable", retryable: true }
15208
+ );
15209
+ } finally {
15210
+ cleanup();
15211
+ }
15212
+ }
15213
+ function authorizationFailureCode(detail) {
15214
+ if (/Only the automation owner/i.test(detail)) return "not_routine_owner";
15215
+ if (/missing required scope/i.test(detail)) return "scope_mismatch";
15216
+ if (/Invalid API key/i.test(detail)) return "invalid_api_key";
15217
+ if (/ERROR_NOT_LOGGED_IN|not logged in/i.test(detail)) return "not_signed_in";
15218
+ return "credential_refused";
15219
+ }
15220
+ async function listServerAgents(deps, options2) {
15221
+ const body = await call(deps, options2.backendUrl, options2.token, SERVER_ROUTINE_PATHS.listAgents, Buffer.alloc(0), options2.timeoutMs);
15222
+ const agents = [];
15223
+ const seen = /* @__PURE__ */ new Set();
15224
+ const fields = decodeFields(body);
15225
+ const truncated = fields.filter((field) => field.number === 3);
15226
+ if (truncated.length > 1 || truncated[0] && truncated[0].wire !== 0) throw protocolFailure2("agent_truncation_invalid");
15227
+ if (truncated[0] && truncated[0].value !== 0n) throw protocolFailure2("agent_roster_truncated");
15228
+ for (const field of fields) {
15229
+ if (field.number !== 1) continue;
15230
+ if (!(field.value instanceof Buffer) || field.wire !== 2) throw protocolFailure2("agent_envelope_invalid");
15231
+ const inner = decodeFields(field.value);
15232
+ const id = text(firstBytes(inner, 12));
15233
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) || seen.has(id)) {
15234
+ throw protocolFailure2("agent_id_invalid");
15235
+ }
15236
+ seen.add(id);
15237
+ const kinds = inner.filter((field2) => field2.number === 19);
15238
+ if (kinds.length > 1 || kinds[0] && kinds[0].wire !== 0) throw protocolFailure2("agent_kind_invalid");
15239
+ const kind = kinds[0]?.value ?? 0n;
15240
+ if (kind !== 0n && kind !== 1n && kind !== 2n) throw protocolFailure2("agent_kind_unknown");
15241
+ const harness = text(firstBytes(inner, 13));
15242
+ const name = text(firstBytes(inner, 3));
15243
+ if (name.length > 512 || /[\x00-\x1f\x7f]/.test(name)) throw protocolFailure2("agent_name_invalid");
15244
+ agents.push({
15245
+ id,
15246
+ name,
15247
+ harness: harness === "temporal" || harness === "box" ? harness : null,
15248
+ isGroup: kind === 2n
15249
+ });
15250
+ if (agents.length > MAX_AGENTS) throw protocolFailure2("too_many_agents");
15251
+ }
15252
+ return agents;
15253
+ }
15254
+ async function listServerRoutines(deps, options2) {
15255
+ const body = await call(
15256
+ deps,
15257
+ options2.backendUrl,
15258
+ options2.token,
15259
+ SERVER_ROUTINE_PATHS.listAutomations,
15260
+ lengthDelimited(1, Buffer.from(options2.agentId, "utf8")),
15261
+ options2.timeoutMs
15262
+ );
15263
+ const records = [];
15264
+ const serverIds = [];
15265
+ const seen = /* @__PURE__ */ new Set();
15266
+ for (const field of decodeFields(body)) {
15267
+ if (field.number !== 1) continue;
15268
+ if (!(field.value instanceof Buffer) || field.wire !== 2) throw protocolFailure2("routine_envelope_invalid");
15269
+ const inner = decodeFields(field.value);
15270
+ const serverId = text(firstBytes(inner, 1));
15271
+ const recordBytes = firstBytes(inner, 2);
15272
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,255}$/.test(serverId) || seen.has(serverId)) {
15273
+ throw protocolFailure2("server_id_invalid");
15274
+ }
15275
+ if (recordBytes === void 0) throw protocolFailure2("record_missing");
15276
+ if (recordBytes.length > MAX_RECORD_BYTES) throw protocolFailure2("record_oversized");
15277
+ const json = text(recordBytes);
15278
+ let record3;
15279
+ try {
15280
+ record3 = JSON.parse(json);
15281
+ } catch {
15282
+ throw protocolFailure2("record_not_json");
15283
+ }
15284
+ if (!isRecord(record3)) throw protocolFailure2("record_not_object");
15285
+ seen.add(serverId);
15286
+ records.push(record3);
15287
+ serverIds.push(serverId);
15288
+ if (records.length > MAX_ROUTINES) throw protocolFailure2("too_many_routines");
15289
+ }
15290
+ try {
15291
+ return projectNativeRoutines(options2.agentId, records).catalog.routines.map((routine, index) => ({
15292
+ ...routine,
15293
+ mutable: false,
15294
+ serverId: serverIds[index],
15295
+ webhookUrl: routine.trigger.type === "webhook" ? webhookUrlFor(options2.backendUrl, serverIds[index]) : null
15296
+ }));
15297
+ } catch (error) {
15298
+ if (error instanceof CliError) throw error;
15299
+ throw protocolFailure2("routine_record_invalid");
15300
+ }
15301
+ }
15302
+ function assertWebhookKey(value) {
15303
+ if (value.length < 1 || Buffer.byteLength(value, "utf8") > MAX_KEY_BYTES || /[\x00-\x20\x7f]/.test(value)) {
15304
+ throw protocolFailure2("key_invalid");
15305
+ }
15306
+ return value;
15307
+ }
15308
+ async function mintWebhookKey(deps, options2) {
15309
+ const body = await call(
15310
+ deps,
15311
+ options2.backendUrl,
15312
+ options2.token,
15313
+ SERVER_ROUTINE_PATHS.mintWebhookKey,
15314
+ lengthDelimited(1, Buffer.from(options2.serverId, "utf8")),
15315
+ options2.timeoutMs
15316
+ );
15317
+ const key = text(firstBytes(decodeFields(body), 1));
15318
+ if (key.length === 0) throw protocolFailure2("key_missing");
15319
+ return assertWebhookKey(key);
15320
+ }
15321
+ async function probeWebhookKey(deps, options2) {
15322
+ if (options2.serverId === NIL_AUTOMATION_ID) throw protocolFailure2("unsafe_probe_target");
15323
+ const controller2 = new AbortController();
15324
+ const abort2 = () => controller2.abort();
15325
+ if (deps.signal?.aborted) controller2.abort();
15326
+ else deps.signal?.addEventListener("abort", abort2, { once: true });
15327
+ const timer = setTimeout(abort2, options2.timeoutMs);
15328
+ try {
15329
+ if (controller2.signal.aborted) throw protocolFailure2("request_cancelled");
15330
+ const response = await deps.fetch(webhookUrlFor(options2.backendUrl, NIL_AUTOMATION_ID), {
15331
+ method: "POST",
15332
+ headers: { authorization: `Bearer ${options2.key}`, "content-type": "application/json" },
15333
+ body: "{}",
15334
+ cache: "no-store",
15335
+ redirect: "manual",
15336
+ signal: controller2.signal
15337
+ });
15338
+ const detail = JSON.parse(text(await readBounded2(response, 64 * 1024)));
15339
+ if (controller2.signal.aborted) throw protocolFailure2("request_cancelled");
15340
+ if (!response.redirected && response.status === 401 && isRecord(detail) && detail.code === "error") {
15341
+ if (detail.message === `User API key missing required scope: automation:${NIL_AUTOMATION_ID}`) return true;
15342
+ if (detail.message === "Invalid API key") return false;
15343
+ }
15344
+ throw protocolFailure2("verification_unknown");
15345
+ } catch (error) {
15346
+ if (error instanceof CliError) throw error;
15347
+ throw protocolFailure2(controller2.signal.aborted ? "request_cancelled" : "verification_unknown");
15348
+ } finally {
15349
+ clearTimeout(timer);
15350
+ deps.signal?.removeEventListener("abort", abort2);
15351
+ }
15352
+ }
15353
+ async function cachedWebhookKey(deps, options2) {
15354
+ if (options2.routine.createdAtMs === null) throw protocolFailure2("routine_incarnation_missing");
15355
+ const scope = createHash8("sha256").update(JSON.stringify([
15356
+ normalizedBackend(options2.backendUrl),
15357
+ createHash8("sha256").update(options2.token).digest("hex"),
15358
+ options2.agentId,
15359
+ options2.routine.serverId,
15360
+ options2.routine.createdAtMs
15361
+ ])).digest("hex");
15362
+ const directory = join11(options2.configDir, "server-webhook-keys");
15363
+ const path = join11(directory, `${scope}.json`);
15364
+ const lockPath = join11(directory, `${scope}.lock`);
15365
+ let lock;
15366
+ let temporary;
15367
+ let preserveLock = false;
15368
+ try {
15369
+ await mkdir9(directory, { recursive: true, mode: 448 });
15370
+ const dir = await lstat5(directory);
15371
+ if (!dir.isDirectory() || (dir.mode & 63) !== 0 || process.getuid && dir.uid !== process.getuid()) throw protocolFailure2("key_store_permissions");
15372
+ lock = await open6(lockPath, constants4.O_CREAT | constants4.O_EXCL | constants4.O_WRONLY | constants4.O_NOFOLLOW, 384);
15373
+ let cached;
15374
+ try {
15375
+ cached = await open6(path, constants4.O_RDONLY | constants4.O_NOFOLLOW);
15376
+ } catch (error) {
15377
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
15378
+ }
15379
+ if (cached) {
15380
+ try {
15381
+ const stat6 = await cached.stat();
15382
+ if (!stat6.isFile() || (stat6.mode & 63) !== 0 || stat6.nlink !== 1 || stat6.size > 8192 || process.getuid && stat6.uid !== process.getuid()) throw protocolFailure2("key_store_permissions");
15383
+ const bytes = Buffer.alloc(8193);
15384
+ const { bytesRead } = await cached.read(bytes, 0, bytes.length, 0);
15385
+ if (bytesRead > 8192) throw protocolFailure2("key_store_corrupt");
15386
+ const record3 = JSON.parse(text(bytes.subarray(0, bytesRead)));
15387
+ if (!isRecord(record3) || record3.version !== 1 || record3.scope !== scope || typeof record3.key !== "string" || Object.keys(record3).length !== 3) throw protocolFailure2("key_store_corrupt");
15388
+ return assertWebhookKey(record3.key);
15389
+ } finally {
15390
+ await cached.close();
15391
+ }
15392
+ }
15393
+ if (deps.signal?.aborted) throw protocolFailure2("request_cancelled");
15394
+ temporary = join11(directory, `${scope}.${process.pid}.${crypto.randomUUID()}.tmp`);
15395
+ const output = await (deps.openWebhookKeyStore ?? open6)(temporary, constants4.O_CREAT | constants4.O_EXCL | constants4.O_WRONLY | constants4.O_NOFOLLOW, 384);
15396
+ try {
15397
+ if (deps.signal?.aborted) throw protocolFailure2("request_cancelled");
15398
+ preserveLock = true;
15399
+ const key = await mintWebhookKey(deps, { ...options2, serverId: options2.routine.serverId });
15400
+ await output.writeFile(JSON.stringify({ version: 1, scope, key }), "utf8");
15401
+ await output.sync();
15402
+ await output.close();
15403
+ await rename6(temporary, path);
15404
+ temporary = void 0;
15405
+ preserveLock = false;
15406
+ return key;
15407
+ } finally {
15408
+ await output.close().catch(() => void 0);
15409
+ }
15410
+ } catch (error) {
15411
+ if (error instanceof CliError && (error.code === "credential_invalid" || error.code === "target_not_found" || error.httpStatus !== void 0 && error.httpStatus >= 400 && error.httpStatus < 500 && error.httpStatus !== 408)) {
15412
+ preserveLock = false;
15413
+ }
15414
+ if (preserveLock) {
15415
+ throw new CliError("operation_outcome_unknown", "Webhook key mint outcome is unknown; no automatic retry is safe. Inspect the protected store lock before another attempt.", {
15416
+ failureCode: "mint_outcome_unknown",
15417
+ retryable: false
15418
+ });
15419
+ }
15420
+ if (error instanceof CliError) throw error;
15421
+ throw protocolFailure2("key_store_unavailable");
15422
+ } finally {
15423
+ if (temporary) await unlink3(temporary).catch(() => void 0);
15424
+ if (lock) {
15425
+ await lock.close();
15426
+ if (!preserveLock) await unlink3(lockPath).catch(() => void 0);
15427
+ }
15428
+ }
15429
+ }
15430
+
14916
15431
  // src/commands/routines.ts
14917
15432
  function routineCliError(error) {
14918
15433
  if (error instanceof CliError) return error;
@@ -14984,7 +15499,8 @@ async function resolveRoutineAuthority(client2, agent, timeoutMs) {
14984
15499
  if (error instanceof CliError && ["gateway_not_found", "gateway_bad_request", "capability_unavailable"].includes(error.code)) {
14985
15500
  throw new CliError(
14986
15501
  "capability_unavailable",
14987
- "This Temporal Bot requires the Grokbox Temporal Routine bridge before Routine state can be read safely."
15502
+ "This Temporal Bot requires the Grokbox Temporal Routine bridge, or an explicit server.access_token_ref in the selected Profile, before Routine state can be read safely.",
15503
+ { failureCode: "routine_bridge_missing" }
14988
15504
  );
14989
15505
  }
14990
15506
  throw error;
@@ -15009,25 +15525,46 @@ async function readSnapshot(client2, agent, timeoutMs) {
15009
15525
  throw new CliError("gateway_conflict", "Gateway generation changed while reading Routines.");
15010
15526
  }
15011
15527
  try {
15528
+ const projected = projectNativeRoutines(agent.id, response.result);
15012
15529
  return {
15013
- ...projectNativeRoutines(agent.id, response.result),
15530
+ ...projected,
15014
15531
  discovery,
15015
- authority: authority.authority
15532
+ authority: authority.authority,
15533
+ prompts: nativePrompts(response.result, projected.catalog)
15016
15534
  };
15017
15535
  } catch (error) {
15018
15536
  throw routineCliError(error);
15019
15537
  }
15020
15538
  }
15021
- function findRoutine(snapshot, id) {
15022
- let rid;
15023
- try {
15024
- rid = routineId(id);
15025
- } catch (error) {
15026
- throw routineCliError(error);
15539
+ function nativePrompts(value, catalog) {
15540
+ if (!Array.isArray(value)) throw new RoutineError("unsupported_shape");
15541
+ const prompts = /* @__PURE__ */ new Map();
15542
+ for (const raw of value) {
15543
+ if (!isRecord(raw) || typeof raw.id !== "string" || typeof raw.prompt !== "string" || raw.prompt.length < 1 || raw.prompt.includes("\0") || Buffer.byteLength(raw.prompt) > 128 * 1024) {
15544
+ throw new RoutineError("unsupported_shape");
15545
+ }
15546
+ prompts.set(raw.id, raw.prompt);
15027
15547
  }
15028
- const row = snapshot.catalog.routines.find((routine) => routine.id === rid);
15029
- if (!row) throw routineCliError(new RoutineError("not_found_in_window"));
15030
- return row;
15548
+ for (const routine of catalog.routines) {
15549
+ if (!prompts.has(routine.id)) throw new RoutineError("unsupported_shape");
15550
+ }
15551
+ return prompts;
15552
+ }
15553
+ function findRoutine(snapshot, target, allowName = false) {
15554
+ const exact = snapshot.catalog.routines.find((routine) => routine.id === target);
15555
+ if (exact) return exact;
15556
+ if (!allowName) {
15557
+ try {
15558
+ routineId(target);
15559
+ } catch (error) {
15560
+ throw routineCliError(error);
15561
+ }
15562
+ throw routineCliError(new RoutineError("not_found_in_window"));
15563
+ }
15564
+ const named = snapshot.catalog.routines.filter((routine) => routine.name.toLocaleLowerCase() === target.toLocaleLowerCase());
15565
+ if (named.length > 1) throw new CliError("target_ambiguous", "Routine name matched more than one Routine.");
15566
+ if (!named[0]) throw routineCliError(new RoutineError("not_found_in_window"));
15567
+ return named[0];
15031
15568
  }
15032
15569
  function mutableRoutine(snapshot, id) {
15033
15570
  const row = findRoutine(snapshot, id);
@@ -15088,7 +15625,7 @@ async function listOrShow(deps, client2, action, agentTarget, routineTarget, raw
15088
15625
  writeSuccess(deps.stdout, { ...snapshot.catalog, authority: snapshot.authority }, snapshot.discovery);
15089
15626
  return;
15090
15627
  }
15091
- const routine = findRoutine(snapshot, routineTarget ?? "");
15628
+ const routine = findRoutine(snapshot, routineTarget ?? "", true);
15092
15629
  writeSuccess(
15093
15630
  deps.stdout,
15094
15631
  { schemaVersion: 1, agentId: agent.id, authority: snapshot.authority, routine, coverage: snapshot.catalog.coverage },
@@ -15281,10 +15818,13 @@ async function setRoutineEnabled(deps, client2, agentTarget, routineTarget, acti
15281
15818
  }
15282
15819
  async function showWebhookCredential(deps, client2, agentTarget, routineTarget, raw) {
15283
15820
  requireConfirmed(raw);
15821
+ if (raw.verify === true) {
15822
+ throw new CliError("capability_unavailable", "Webhook key verification requires an explicit cloud credential in the selected Profile; Host-provided credentials are not sent to additional endpoints.");
15823
+ }
15284
15824
  const io = ioFromOpts(raw);
15285
15825
  const agent = await resolveAgent(client2, agentTarget, io.timeoutMs);
15286
15826
  const snapshot = await readSnapshot(client2, agent, io.timeoutMs);
15287
- const routine = findRoutine(snapshot, routineTarget);
15827
+ const routine = findRoutine(snapshot, routineTarget, true);
15288
15828
  if (routine.trigger.type !== "webhook") {
15289
15829
  throw new CliError("capability_unavailable", "Routine is not webhook-triggered.");
15290
15830
  }
@@ -15305,6 +15845,10 @@ async function showWebhookCredential(deps, client2, agentTarget, routineTarget,
15305
15845
  if (url.protocol !== "https:" || url.username || url.password || url.hash) {
15306
15846
  throw new CliError("gateway_internal", "Gateway returned an unsafe webhook URL.");
15307
15847
  }
15848
+ const prompt = snapshot.prompts.get(routine.id);
15849
+ if (prompt === void 0) {
15850
+ throw new CliError("gateway_internal", "Gateway returned a Routine without a prompt.");
15851
+ }
15308
15852
  const revealKey = raw.revealKey === true;
15309
15853
  writeSuccess(deps.stdout, {
15310
15854
  schemaVersion: 1,
@@ -15314,7 +15858,8 @@ async function showWebhookCredential(deps, client2, agentTarget, routineTarget,
15314
15858
  url: url.toString(),
15315
15859
  keyPresent: true,
15316
15860
  keyIncluded: revealKey,
15317
- ...revealKey ? { key: response.result.key } : {}
15861
+ promptIncluded: revealKey,
15862
+ ...revealKey ? { key: response.result.key, prompt } : {}
15318
15863
  }, discovery);
15319
15864
  }
15320
15865
  async function deleteRoutine(deps, client2, agentTarget, routineTarget, raw) {
@@ -15359,7 +15904,58 @@ async function deleteRoutine(deps, client2, agentTarget, routineTarget, raw) {
15359
15904
  })
15360
15905
  }, after.discovery);
15361
15906
  }
15907
+ async function cloudRoutines(deps, action, agentTarget, routineTarget, raw) {
15908
+ if (action === "webhook-credential") requireConfirmed(raw);
15909
+ const io = ioFromOpts(raw);
15910
+ const context = {
15911
+ backendUrl: normalizedBackend(deps.serverBackendUrl ?? DEFAULT_SERVER_BACKEND_URL),
15912
+ token: await resolveSecretRef(deps, deps.serverAccessTokenRef),
15913
+ timeoutMs: io.timeoutMs
15914
+ };
15915
+ const agents = await listServerAgents(deps, context);
15916
+ const agent = findRosterRow(agents, agentTarget, ["agent"]);
15917
+ const agentId = asString(agent.id);
15918
+ const routines = await listServerRoutines(deps, { ...context, agentId });
15919
+ const coverage = { kind: "native_returned_window", limit: 100, atLimit: routines.length === 100, complete: false };
15920
+ const catalog = { schemaVersion: 1, agentId, routines, coverage };
15921
+ if (action === "list") {
15922
+ if (io.table) deps.stdout.write(listTable(catalog));
15923
+ else writeSuccess(deps.stdout, { ...catalog, authority: "server-temporal" });
15924
+ return;
15925
+ }
15926
+ let selected = routines.filter((routine2) => routine2.id === routineTarget || routine2.serverId === routineTarget);
15927
+ if (selected.length === 0) {
15928
+ selected = routines.filter((routine2) => routine2.name.toLocaleLowerCase() === (routineTarget ?? "").toLocaleLowerCase());
15929
+ }
15930
+ if (selected.length > 1) throw new CliError("target_ambiguous", "Routine target matched more than one Routine.");
15931
+ const routine = selected[0];
15932
+ if (!routine) throw new CliError("target_not_found", "No Routine matched that ID or name in the configured cloud account; no other identity was searched.");
15933
+ if (action === "show") {
15934
+ writeSuccess(deps.stdout, { schemaVersion: 1, agentId, authority: "server-temporal", routine, coverage });
15935
+ return;
15936
+ }
15937
+ if (routine.trigger.type !== "webhook") throw new CliError("capability_unavailable", "Routine is not webhook-triggered.");
15938
+ const key = await cachedWebhookKey(deps, { ...context, configDir: deps.configDir, agentId, routine });
15939
+ const keyLive = raw.verify === true ? await probeWebhookKey(deps, { ...context, key, serverId: routine.serverId }) : void 0;
15940
+ writeSuccess(deps.stdout, {
15941
+ schemaVersion: 1,
15942
+ agentId,
15943
+ routineId: routine.id,
15944
+ serverId: routine.serverId,
15945
+ authority: "server-temporal",
15946
+ url: routine.webhookUrl,
15947
+ keyPresent: true,
15948
+ keyIncluded: raw.revealKey === true,
15949
+ promptIncluded: false,
15950
+ ...keyLive === void 0 ? {} : { keyLive },
15951
+ ...raw.revealKey === true ? { key } : {}
15952
+ });
15953
+ }
15362
15954
  async function runRoutines(deps, action, agentTarget, routineTarget, raw) {
15955
+ if (deps.serverAccessTokenRef !== void 0 && (action === "list" || action === "show" || action === "webhook-credential")) {
15956
+ await cloudRoutines(deps, action, agentTarget, routineTarget, raw);
15957
+ return;
15958
+ }
15363
15959
  const client2 = new GatewayClient(deps);
15364
15960
  if (action === "list" || action === "show") {
15365
15961
  await listOrShow(deps, client2, action, agentTarget, routineTarget, raw);
@@ -15429,11 +16025,11 @@ async function runSend(deps, target, raw) {
15429
16025
  }
15430
16026
 
15431
16027
  // src/skills.ts
15432
- import { join as join11 } from "node:path";
16028
+ import { join as join12 } from "node:path";
15433
16029
  var CORE_SKILL_NAME = "core";
15434
16030
  var CORE_SKILL_SUMMARY = "Core grokbox usage guide.";
15435
16031
  async function loadCoreMarkdown(deps, full) {
15436
- const overview = await deps.readFile(join11(deps.skillsDir, "core.md"));
16032
+ const overview = await deps.readFile(join12(deps.skillsDir, "core.md"));
15437
16033
  if (!full) return overview.trimEnd() + "\n";
15438
16034
  return `${overview.trimEnd()}
15439
16035
 
@@ -15496,9 +16092,9 @@ var FAMILY_DESCRIPTIONS = {
15496
16092
  is: "Read state projections"
15497
16093
  };
15498
16094
  function publicCommanderMessage(error) {
15499
- const text = error.message.replace(/^error: /i, "").trim();
15500
- if (/bearer|authorization|token/i.test(text)) return "Invalid usage.";
15501
- return text.length > 0 ? text : "Invalid usage.";
16095
+ const text2 = error.message.replace(/^error: /i, "").trim();
16096
+ if (/bearer|authorization|token/i.test(text2)) return "Invalid usage.";
16097
+ return text2.length > 0 ? text2 : "Invalid usage.";
15502
16098
  }
15503
16099
  function unexpectedMessage(error) {
15504
16100
  if (error instanceof Error) {
@@ -15637,6 +16233,8 @@ function addLeaf(deps, program2, parents, leaf, action) {
15637
16233
  sandboxKeepaliveIntervalMs: profile.sandbox?.keepalive_interval_ms,
15638
16234
  quotaSource: profile.quota?.source,
15639
16235
  quotaAccessTokenRef: profile.quota?.access_token_ref,
16236
+ serverAccessTokenRef: profile.server?.access_token_ref,
16237
+ serverBackendUrl: profile.server?.backend_url,
15640
16238
  gatewayServerUrl: profile.gateway_url,
15641
16239
  gatewayTokenRef: profile.gateway_token_ref,
15642
16240
  gatewayHeadersRef: profile.gateway_headers_ref,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grokbox",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "description": "Unofficial CLI and control plane for Grok Bot cloud computers",
6
6
  "license": "MIT",
package/skills/core.md CHANGED
@@ -41,6 +41,10 @@ grokbox box wake
41
41
  grokbox box keepalive status
42
42
  grokbox agents list --table
43
43
  grokbox agents routines list <agent> --table
44
+ grokbox profile update <name> --server-access-token-ref file:/absolute/path/to/cloud-token
45
+ grokbox profile use <name>
46
+ grokbox agents routines show <agent> <routine-id>
47
+ grokbox agents routines webhook credential <agent> <routine-id> --confirm --verify
44
48
  grokbox groups list --table
45
49
  grokbox send <target> --text "hello"
46
50
  grokbox history tail <target> --limit 50
@@ -72,7 +76,8 @@ Targets accept an exact ID first, then an unambiguous case-insensitive name/titl
72
76
  `groups list/show/create/update/delete` only own product groups. Membership is only
73
77
  `groups members list/add/remove/set`; every member must resolve to a non-group agent and a group
74
78
  must retain 1-6 unique members.
75
- 3. **Manage native Routines**: `agents routines list/show` returns bounded metadata only and never emits prompt bodies, credentials, or native paths. Box-local Bots support create/update/enable/disable/delete; create writes a disabled definition and later mutations require `--expect-revision` from a fresh read plus `--confirm`. Temporal Bots are Server-authoritative: list/show, webhook credential, enable/disable, and delete require the bounded Temporal Routine Host bridge; create/update refuse because the current official Temporal contract exposes neither mutation. `agents routines webhook credential <agent> <routine-id> --confirm` hides the key by default; `--reveal-key` is the explicit secret-output boundary. Without the bridge, Temporal reads fail closed instead of reporting the legacy local store. Native writes have no upstream CAS or idempotency guarantee; uncertain writes return `operation_outcome_unknown` and must be reconciled by another read, never automatic replay.
79
+ 3. **Manage Routines through one interface**: `agents routines list/show` and `agents routines webhook credential <agent> <routine-id>` use the selected box Profile's cloud SSOT whenever `server.access_token_ref` is configured. Configure with `profile add/update --server-access-token-ref` and optional `--server-backend-url`, using a protected secret reference. Cloud reads preserve envelopes and bounded metadata with `serverId`, webhook URL, and `mutable:false`, never keys or prompts. The Server-returned automation ID is authoritative. There is no separate command root, source flag, desktop discovery, or roster union. First credential access needs `--confirm` to mint; repeated access reuses a local cache scoped to backend, credential digest, agent, Server ID, and creation time. Minting is never automatically retried. `--reveal-key` explicitly reveals the key, never a cloud prompt. `--verify` probes only a fixed, distinct nonexistent ID, requires strict 401 `missing required scope` evidence, supports abort, and never POSTs to the real Routine. Cloud failures are reported directly: check the selected Profile reference/backend and authorization/protocol error, not another route.
80
+ Without cloud configuration, Gateway/Host behavior remains, including fail-closed missing Temporal bridge checks. Mutations retain the Host contract: Box-local create is disabled, later mutations require fresh `--expect-revision` and `--confirm`, and unsupported Temporal create/update refuse. Cloud adds no CRUD. Unknown write outcomes require reconciliation, not automatic replay. Host credential disclosure retains its existing contract. Cloud authority grants no Gateway, daemon, Sandbox, or quota permissions.
76
81
  4. **Search/read transcript**: `history search <query>`, `history tail <target>`, and
77
82
  `history thread <target> --root <entry-id>` own transcript access. Object `show` commands do
78
83
  not add transcript side projections.