grokbox 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ 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
- > **Release:** current source is `0.1.0`. Stable versions publish to npm
13
+ > **Release:** current source is `0.1.1`. Stable versions publish to npm
14
14
  > `latest`; prereleases use `next`. Upstream-private compatibility adapters
15
15
  > remain experimental and subject to the documented acceptance limits.
16
16
 
@@ -47,7 +47,7 @@ never gain host filesystem or process authority.
47
47
  ## Prerequisites and platform support
48
48
 
49
49
  - Node.js 20+ for the published-style CLI runtime.
50
- - Bun 1.3.14 for source development and the pre-release source shim.
50
+ - Bun 1.3.14 for source development and the source-backed shim.
51
51
  - An existing Grok Bot cloud computer that you own or are authorized to use.
52
52
  - Tailscale plus BatchMode SSH for remote bootstrap/recovery; bootstrap also
53
53
  needs npm or Bun locally to repack the installed runtime for transfer.
@@ -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. 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.
150
+ Routine reads expose bounded metadata only, never prompts, keys, native paths, or unknown provider fields. Version 0.1.1 automatically uses proven current-box native Host authentication for `agents routines list/show` and `agents routines webhook credential <agent> <routine-id> --confirm`, without manual cloud token setup. This supports only the observed packaged production Linux Host layout described in the [native identity boundary](docs/upstream-integration.md#server-of-record-routine-surface); custom backends and lab/dev Hosts fail as unsupported. Explicit Profile credentials remain authoritative, including on rejection; remote Profiles never borrow local identity. Missing native proof retains Gateway/Host behavior, not false empty Temporal lists; authentication failures never fall back. Confirmation gates credential side effects, and the key cache remains stable across token renewal. `--reveal-key` reveals only the key; `--verify` never invokes the real Routine. Mutations retain the Host contract; capabilities remain conditional, not proof of authorization. Source-first real-box validation passed with the default Profile and no token injection: both CLI names read Routine list/show with the public Bot UUID and current cloud webhook URL; unknown Routine lookup returned exit 17. No real Routine was invoked and no webhook key was minted. See the [Routine contract](docs/product-contract.md#711-native-routines).
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
- > **发布:** 当前源码版本是 `0.1.0`。正式版发布到 npm `latest`,预发布版使用 `next`。上游私有接口的兼容适配器仍为实验性,遵守文档中的验收范围与限制。
9
+ > **发布:** 当前源码版本是 `0.1.1`。正式版发布到 npm `latest`,预发布版使用 `next`。上游私有接口的兼容适配器仍为实验性,遵守文档中的验收范围与限制。
10
10
 
11
11
  ## 它能做什么
12
12
 
@@ -40,7 +40,7 @@ Daemon 不提供通用 raw RPC 或任意 shell。仅有 Gateway 权限的 Profil
40
40
  ## 前置条件与平台支持
41
41
 
42
42
  - 发布版 CLI 运行时需要 Node.js 20+。
43
- - 源码开发与发布前源码 shim 需要 Bun 1.3.14。
43
+ - 源码开发与源码 shim 需要 Bun 1.3.14。
44
44
  - 需要一个你拥有或获准使用的 Grok Bot 云电脑。
45
45
  - 远程 bootstrap/recovery 需要 Tailscale 与 BatchMode SSH;bootstrap 还需要本机 npm 或 Bun,以便重新打包已安装运行时并传输。
46
46
 
@@ -128,7 +128,7 @@ grokbox jobs list --table
128
128
  grokbox desktop status --table
129
129
  ```
130
130
 
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)。
131
+ Routine 读取只输出有界元数据,不暴露 prompt、key、原生路径或未知 provider 字段。0.1.1 通过已证明归属的当前 box 原生 Host 身份,自动使用同一组 `agents routines list/show` 与 `agents routines webhook credential <agent> <routine-id> --confirm` 命令,无需手工配置 cloud token。仅支持[精确原生身份边界](docs/upstream-integration.md#server-of-record-routine-surface)中的已观察 packaged production Linux Host 布局;自定义 backend、lab/dev Host 以 unsupported 拒绝。显式 Profile 凭据优先,被拒绝后不 fallback;Remote Profile 不能借用本机身份。原生证明不可用时保留 Gateway/Host 行为,缺失 Temporal bridge 必须拒绝而非返回虚假空列表。确认后才发生 credential 副作用,key 缓存身份不随 token 续期变化;`--reveal-key` 只揭示 key,`--verify` 不调用真实 Routine。Mutation 保留 Host 合同;capabilities 仍为条件依赖,不宣称已授权。源码优先实机验收已通过:默认 Profile、无 token 注入下,两个 CLI 名称均可读取公开 Bot UUID 对应的 Routine list/show 与当前云端 webhook URL;未知 Routine 返回 exit 17。未调用真实 Routine,也未创建 webhook key。详见 [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
@@ -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",
3477
+ version: "0.1.1",
3478
3478
  packageManager: "bun@1.3.14",
3479
3479
  description: "Unofficial CLI and control plane for Grok Bot cloud computers",
3480
3480
  license: "MIT",
@@ -7557,11 +7557,11 @@ function jitteredInterval(intervalMs, randomUUID7) {
7557
7557
  const factor = 1 - JITTER_RATIO + sample * JITTER_RATIO * 2;
7558
7558
  return Math.max(1e3, Math.round(intervalMs * factor));
7559
7559
  }
7560
- function failureDelay(intervalMs, failure, consecutiveFailures, retryAfterMs) {
7561
- if (failure === "rate_limited") {
7560
+ function failureDelay(intervalMs, failure2, consecutiveFailures, retryAfterMs) {
7561
+ if (failure2 === "rate_limited") {
7562
7562
  return Math.max(intervalMs, retryAfterMs ?? FAILURE_BASE_BACKOFF_MS);
7563
7563
  }
7564
- if (failure === "provider_unavailable" || failure === "request_timeout") {
7564
+ if (failure2 === "provider_unavailable" || failure2 === "request_timeout") {
7565
7565
  const backoff = Math.min(
7566
7566
  FAILURE_BASE_BACKOFF_MS * 2 ** Math.max(0, consecutiveFailures - 1),
7567
7567
  FAILURE_MAX_BACKOFF_MS
@@ -14914,6 +14914,7 @@ async function runProfileCapabilities(deps, name, raw) {
14914
14914
  const sandboxCapability = sandboxConfigured ? "provider-authorization-dependent" : false;
14915
14915
  const quotaCapability = profile.quota?.access_token_ref ? "provider-authorization-dependent" : false;
14916
14916
  const serverCapability = profile.server?.access_token_ref ? "provider-authorization-dependent" : false;
14917
+ const routineCapability = serverCapability || (local || directGateway || daemon ? "runtime-host-dependent" : false);
14917
14918
  writeSuccess(deps.stdout, {
14918
14919
  profile: profile.name,
14919
14920
  transport: profile.transport,
@@ -14941,12 +14942,17 @@ async function runProfileCapabilities(deps, name, raw) {
14941
14942
  "sandbox.wake": sandboxCapability,
14942
14943
  "sandbox.keepalive": sandboxCapability,
14943
14944
  "quota.read": quotaCapability,
14944
- "grok.routine.read": serverCapability || local || directGateway || daemon,
14945
- "grok.routine.webhook": serverCapability || local || directGateway || daemon
14945
+ "grok.routine.read": routineCapability,
14946
+ "grok.routine.webhook": routineCapability
14946
14947
  }
14947
14948
  });
14948
14949
  }
14949
14950
 
14951
+ // src/box-routine-auth.ts
14952
+ import { createHash as createHash9 } from "node:crypto";
14953
+ import { constants as constants5 } from "node:fs";
14954
+ import { open as open7, stat as stat6 } from "node:fs/promises";
14955
+
14950
14956
  // src/server-routines.ts
14951
14957
  import { createHash as createHash8 } from "node:crypto";
14952
14958
  import { constants as constants4 } from "node:fs";
@@ -15354,7 +15360,7 @@ async function cachedWebhookKey(deps, options2) {
15354
15360
  if (options2.routine.createdAtMs === null) throw protocolFailure2("routine_incarnation_missing");
15355
15361
  const scope = createHash8("sha256").update(JSON.stringify([
15356
15362
  normalizedBackend(options2.backendUrl),
15357
- createHash8("sha256").update(options2.token).digest("hex"),
15363
+ options2.credentialScope ?? createHash8("sha256").update(options2.token).digest("hex"),
15358
15364
  options2.agentId,
15359
15365
  options2.routine.serverId,
15360
15366
  options2.routine.createdAtMs
@@ -15378,8 +15384,8 @@ async function cachedWebhookKey(deps, options2) {
15378
15384
  }
15379
15385
  if (cached) {
15380
15386
  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");
15387
+ const stat7 = await cached.stat();
15388
+ if (!stat7.isFile() || (stat7.mode & 63) !== 0 || stat7.nlink !== 1 || stat7.size > 8192 || process.getuid && stat7.uid !== process.getuid()) throw protocolFailure2("key_store_permissions");
15383
15389
  const bytes = Buffer.alloc(8193);
15384
15390
  const { bytesRead } = await cached.read(bytes, 0, bytes.length, 0);
15385
15391
  if (bytesRead > 8192) throw protocolFailure2("key_store_corrupt");
@@ -15428,6 +15434,178 @@ async function cachedWebhookKey(deps, options2) {
15428
15434
  }
15429
15435
  }
15430
15436
 
15437
+ // src/box-routine-auth.ts
15438
+ var ENV_LIMIT = 256 * 1024;
15439
+ var RESPONSE_LIMIT = 32 * 1024;
15440
+ async function boundedFile(path, limit) {
15441
+ const handle = await open7(path, constants5.O_RDONLY | constants5.O_NOFOLLOW);
15442
+ try {
15443
+ const buffer = Buffer.alloc(limit + 1);
15444
+ let length = 0;
15445
+ while (length < buffer.length) {
15446
+ const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
15447
+ if (!bytesRead) break;
15448
+ length += bytesRead;
15449
+ }
15450
+ if (length > limit) throw new Error("bounded process read");
15451
+ return buffer.subarray(0, length);
15452
+ } finally {
15453
+ await handle.close();
15454
+ }
15455
+ }
15456
+ var productionIO = {
15457
+ platform: process.platform,
15458
+ uid: process.getuid?.() ?? -1,
15459
+ async process(pid) {
15460
+ const root = `/proc/${pid}`;
15461
+ const metadata = await stat6(root);
15462
+ const status = (await boundedFile(`${root}/status`, 32 * 1024)).toString("utf8");
15463
+ const uids = /^Uid:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$/m.exec(status);
15464
+ if (!uids || uids.slice(1).some((uid) => Number(uid) !== metadata.uid)) throw new Error("process owner");
15465
+ const raw = (await boundedFile(`${root}/stat`, 8192)).toString("utf8");
15466
+ const start = raw.slice(raw.lastIndexOf(")") + 2).split(" ")[19];
15467
+ if (!start || !/^\d+$/.test(start)) throw new Error("process start");
15468
+ return { uid: metadata.uid, start, cmdline: (await boundedFile(`${root}/cmdline`, 8192)).toString("utf8") };
15469
+ },
15470
+ environment: async (pid) => await boundedFile(`/proc/${pid}/environ`, ENV_LIMIT)
15471
+ };
15472
+ function nativeRoutineAuthEligible(deps) {
15473
+ return (deps.boxRoutineAuthIO ?? productionIO).platform === "linux" && (deps.transport === "auto" || deps.transport === "local") && !deps.gatewayServerUrl && !deps.daemonServerUrl && !deps.sshHost;
15474
+ }
15475
+ function validHost(value, uid) {
15476
+ const args = value.cmdline.split("\0");
15477
+ if (args.at(-1) === "") args.pop();
15478
+ return uid >= 0 && value.uid === uid && /^\d+$/.test(value.start) && args[0] === "/exec-daemon/node" && args.at(-1) === "/opt/sand/sand-host/host-main.cjs" && args.slice(1, -1).every((arg) => arg === "--disable-warning=ExperimentalWarning");
15479
+ }
15480
+ function sameDiscovery(a, b) {
15481
+ return a.pid === b.pid && a.startedAt === b.startedAt && a.baseUrl === b.baseUrl && a.token === b.token;
15482
+ }
15483
+ function failure(code) {
15484
+ return new CliError("credential_invalid", "Current-box Routine authentication could not be established safely.", { failureCode: code });
15485
+ }
15486
+ async function resolveBoxRoutineAuth(deps, timeoutMs) {
15487
+ if (!nativeRoutineAuthEligible(deps)) return null;
15488
+ if (deps.signal?.aborted) throw failure("request_cancelled");
15489
+ const io = deps.boxRoutineAuthIO ?? productionIO;
15490
+ let discovery;
15491
+ let environment;
15492
+ try {
15493
+ discovery = await readDiscovery(deps);
15494
+ if (!Number.isSafeInteger(discovery.pid) || discovery.pid <= 0 || !Number.isSafeInteger(discovery.startedAt) || discovery.startedAt <= 0) return null;
15495
+ const before = await io.process(discovery.pid);
15496
+ if (!validHost(before, io.uid)) return null;
15497
+ const healthy = await new GatewayClient({ ...deps, transport: "local" }).health(timeoutMs);
15498
+ if (!sameDiscovery(discovery, healthy.discovery) || healthy.health.pid !== discovery.pid || healthy.health.startedAt !== discovery.startedAt) return null;
15499
+ environment = await io.environment(discovery.pid);
15500
+ const after = await io.process(discovery.pid);
15501
+ if (!validHost(after, io.uid) || before.start !== after.start || before.cmdline !== after.cmdline || !sameDiscovery(discovery, await readDiscovery(deps))) return null;
15502
+ } catch {
15503
+ if (deps.signal?.aborted) throw failure("request_cancelled");
15504
+ return null;
15505
+ }
15506
+ if (environment.length > ENV_LIMIT) throw failure("renewal_credential_oversized");
15507
+ const entries = environment.toString("utf8").split("\0").filter((entry) => entry.startsWith("SAND_INFERENCE_RENEWAL_CREDENTIAL="));
15508
+ if (entries.length === 0) return null;
15509
+ const credential = entries[0].slice("SAND_INFERENCE_RENEWAL_CREDENTIAL=".length).trim();
15510
+ if (entries.length !== 1 || !credential || Buffer.byteLength(credential) > 16 * 1024 || /[\r\n]/.test(credential)) {
15511
+ throw failure("renewal_credential_invalid");
15512
+ }
15513
+ const metadata = {};
15514
+ for (const entry of environment.toString("utf8").split("\0")) {
15515
+ const separator = entry.indexOf("=");
15516
+ const key = entry.slice(0, separator);
15517
+ if (!["SAND_CLIENT_APP_VERSION", "SAND_BOX_OWNER_NAMESPACE", "SAND_BACKEND_URL", "CURSOR_API_BASE_URL", "SAND_PACKAGED", "SAND_LAB"].includes(key)) continue;
15518
+ const value = entry.slice(separator + 1);
15519
+ if (metadata[key] !== void 0 || value.length > 1024 || /[^\x20-\x7e]/.test(value)) throw failure("renewal_metadata_invalid");
15520
+ metadata[key] = value;
15521
+ }
15522
+ for (const key of ["SAND_BACKEND_URL", "CURSOR_API_BASE_URL"]) {
15523
+ const origin = metadata[key];
15524
+ if (origin && origin !== DEFAULT_SERVER_BACKEND_URL && origin !== `${DEFAULT_SERVER_BACKEND_URL}/`) throw failure("renewal_backend_unsupported");
15525
+ }
15526
+ const ownerNamespace = metadata.SAND_BOX_OWNER_NAMESPACE?.trim();
15527
+ if (metadata.SAND_PACKAGED !== "1" || metadata.SAND_LAB === "1" || ownerNamespace === "dev" || ownerNamespace === "lab") {
15528
+ throw failure("renewal_namespace_unsupported");
15529
+ }
15530
+ const namespace = "prod";
15531
+ const clientVersion = /^(\d+\.\d+\.\d+)(?:-.+)?$/.exec(metadata.SAND_CLIENT_APP_VERSION?.trim() ?? "")?.[1] ?? "0.1.0";
15532
+ const credentialScope = createHash9("sha256").update(JSON.stringify([
15533
+ "box-routine-auth-v1",
15534
+ DEFAULT_SERVER_BACKEND_URL,
15535
+ namespace,
15536
+ credential
15537
+ ])).digest("hex");
15538
+ const controller2 = new AbortController();
15539
+ const abort2 = () => controller2.abort();
15540
+ deps.signal?.addEventListener("abort", abort2, { once: true });
15541
+ const timer = setTimeout(abort2, timeoutMs);
15542
+ try {
15543
+ if (deps.signal?.aborted) controller2.abort();
15544
+ if (controller2.signal.aborted) throw failure("request_cancelled");
15545
+ const response = await deps.fetch(`${DEFAULT_SERVER_BACKEND_URL}/sand-box/inference-credential`, {
15546
+ method: "POST",
15547
+ headers: {
15548
+ "content-type": "application/json",
15549
+ "x-cursor-client-type": "sand",
15550
+ "x-cursor-client-source": "sand-desktop",
15551
+ "x-cursor-client-version": clientVersion,
15552
+ "x-sand-box-namespace": namespace
15553
+ },
15554
+ body: JSON.stringify({ credential }),
15555
+ redirect: "manual",
15556
+ cache: "no-store",
15557
+ signal: controller2.signal
15558
+ });
15559
+ if (response.redirected || !response.ok) {
15560
+ await response.body?.cancel().catch(() => void 0);
15561
+ throw failure(response.status >= 300 && response.status < 400 ? "renewal_redirect_refused" : response.status === 401 || response.status === 403 ? "renewal_unauthorized" : "renewal_unavailable");
15562
+ }
15563
+ const declared = response.headers.get("content-length");
15564
+ if (declared !== null && (!Number.isSafeInteger(Number(declared)) || Number(declared) < 0 || Number(declared) > RESPONSE_LIMIT)) {
15565
+ await response.body?.cancel().catch(() => void 0);
15566
+ throw failure("renewal_response_oversized");
15567
+ }
15568
+ const reader = response.body?.getReader();
15569
+ const chunks = [];
15570
+ let size = 0;
15571
+ try {
15572
+ if (reader) for (; ; ) {
15573
+ const next = await reader.read();
15574
+ if (next.done) break;
15575
+ size += next.value.byteLength;
15576
+ if (size > RESPONSE_LIMIT) {
15577
+ await reader.cancel().catch(() => void 0);
15578
+ throw failure("renewal_response_oversized");
15579
+ }
15580
+ chunks.push(Buffer.from(next.value));
15581
+ }
15582
+ } finally {
15583
+ reader?.releaseLock();
15584
+ }
15585
+ if (controller2.signal.aborted) throw failure("request_cancelled");
15586
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
15587
+ let expiresAtMs = isRecord(result) ? result.expiresAtMs : void 0;
15588
+ if (expiresAtMs === void 0 && isRecord(result) && typeof result.accessToken === "string") {
15589
+ try {
15590
+ const payload = JSON.parse(Buffer.from(result.accessToken.split(".")[1] ?? "", "base64url").toString("utf8"));
15591
+ if (isRecord(payload) && typeof payload.exp === "number") expiresAtMs = payload.exp * 1e3;
15592
+ } catch {
15593
+ }
15594
+ }
15595
+ if (!isRecord(result) || typeof result.accessToken !== "string" || !result.accessToken || Buffer.byteLength(result.accessToken) > 16 * 1024 || /[\s\0]/.test(result.accessToken) || typeof expiresAtMs !== "number" || !Number.isSafeInteger(expiresAtMs) || expiresAtMs <= deps.now() + 3e4) {
15596
+ throw failure("renewal_response_invalid");
15597
+ }
15598
+ return { backendUrl: DEFAULT_SERVER_BACKEND_URL, token: result.accessToken, credentialScope };
15599
+ } catch (error) {
15600
+ if (controller2.signal.aborted) throw failure("request_cancelled");
15601
+ if (error instanceof CliError) throw error;
15602
+ throw failure("renewal_response_invalid");
15603
+ } finally {
15604
+ clearTimeout(timer);
15605
+ deps.signal?.removeEventListener("abort", abort2);
15606
+ }
15607
+ }
15608
+
15431
15609
  // src/commands/routines.ts
15432
15610
  function routineCliError(error) {
15433
15611
  if (error instanceof CliError) return error;
@@ -15904,10 +16082,10 @@ async function deleteRoutine(deps, client2, agentTarget, routineTarget, raw) {
15904
16082
  })
15905
16083
  }, after.discovery);
15906
16084
  }
15907
- async function cloudRoutines(deps, action, agentTarget, routineTarget, raw) {
16085
+ async function cloudRoutines(deps, action, agentTarget, routineTarget, raw, nativeAuth) {
15908
16086
  if (action === "webhook-credential") requireConfirmed(raw);
15909
16087
  const io = ioFromOpts(raw);
15910
- const context = {
16088
+ const context = nativeAuth ? { ...nativeAuth, timeoutMs: io.timeoutMs } : {
15911
16089
  backendUrl: normalizedBackend(deps.serverBackendUrl ?? DEFAULT_SERVER_BACKEND_URL),
15912
16090
  token: await resolveSecretRef(deps, deps.serverAccessTokenRef),
15913
16091
  timeoutMs: io.timeoutMs
@@ -15956,6 +16134,14 @@ async function runRoutines(deps, action, agentTarget, routineTarget, raw) {
15956
16134
  await cloudRoutines(deps, action, agentTarget, routineTarget, raw);
15957
16135
  return;
15958
16136
  }
16137
+ if (action === "webhook-credential") requireConfirmed(raw);
16138
+ if (action === "list" || action === "show" || action === "webhook-credential") {
16139
+ const nativeAuth = await resolveBoxRoutineAuth(deps, ioFromOpts(raw).timeoutMs);
16140
+ if (nativeAuth) {
16141
+ await cloudRoutines(deps, action, agentTarget, routineTarget, raw, nativeAuth);
16142
+ return;
16143
+ }
16144
+ }
15959
16145
  const client2 = new GatewayClient(deps);
15960
16146
  if (action === "list" || action === "show") {
15961
16147
  await listOrShow(deps, client2, action, agentTarget, routineTarget, raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grokbox",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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,8 +41,6 @@ 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
44
  grokbox agents routines show <agent> <routine-id>
47
45
  grokbox agents routines webhook credential <agent> <routine-id> --confirm --verify
48
46
  grokbox groups list --table
@@ -76,8 +74,8 @@ Targets accept an exact ID first, then an unambiguous case-insensitive name/titl
76
74
  `groups list/show/create/update/delete` only own product groups. Membership is only
77
75
  `groups members list/add/remove/set`; every member must resolve to a non-group agent and a group
78
76
  must retain 1-6 unique members.
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.
77
+ 3. **Manage Routines through one interface**: `agents routines list/show` and `agents routines webhook credential <agent> <routine-id> --confirm` use explicit selected Profile `server.access_token_ref` first, including on rejection with no fallback. Supported local Linux `auto`/local contexts automatically use proven current-box native Host auth without manual token setup. Only the observed packaged production argv is supported: `/exec-daemon/node`, optional exact `--disable-warning=ExperimentalWarning` arguments, then `/opt/sand/sand-host/host-main.cjs`, with `SAND_PACKAGED=1` and production backend metadata. Custom backends and lab/dev Hosts fail as unsupported. External use may configure `profile add/update --server-access-token-ref` and `--server-backend-url`. Remote Profiles cannot borrow local identity. Exact loopback discovery PID/start generation and health, same-UID Host command line and generation before/after bounded allowlisted environment access are required; never scan all processes, read private App secrets, use identity sockets, or merge accounts by name. Renew only through the fixed inference endpoint; keep account tokens in memory, never treat `grokBotToken` as account auth. Missing proof retains Gateway/Host behavior, including fail-closed missing Temporal bridge checks; renewal/provider/auth errors never fall back. Default-Profile source-first real-box list/show validation passed without injected tokens; no real Routine was invoked and no webhook key was minted.
78
+ Cloud reads preserve bounded metadata with `serverId`, webhook URL, and `mutable:false`, never keys or prompts. The Server automation ID is authoritative. `--confirm` precedes credential side effects; repeated access reuses a scoped key cache whose native renewal identity remains stable across token renewal. Minting is never automatically retried. `--reveal-key` reveals only the cloud key; `--verify` probes only a distinct nonexistent ID, requires strict 401 `missing required scope`, and never POSTs to the real Routine. No new server command, `--via`, cloud CRUD, or roster union exists. Mutations retain Host revision/confirmation contracts and unsupported Temporal create/update refuse. Static `profile capabilities` is config-only: explicit cloud config is `provider-authorization-dependent`, eligible Host transport is `runtime-host-dependent`, never proven authorization or a native secret/process/network probe. Runtime commands establish availability. Cloud authority grants no Gateway, daemon, Sandbox, or quota permissions.
81
79
  4. **Search/read transcript**: `history search <query>`, `history tail <target>`, and
82
80
  `history thread <target> --root <entry-id>` own transcript access. Object `show` commands do
83
81
  not add transcript side projections.