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 +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/index.js +197 -11
- package/package.json +1 -1
- package/skills/core.md +2 -4
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
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,
|
|
7561
|
-
if (
|
|
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 (
|
|
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":
|
|
14945
|
-
"grok.routine.webhook":
|
|
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
|
|
15382
|
-
if (!
|
|
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
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
|
|
80
|
-
|
|
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.
|