grokbot-control 0.0.0-stage → 0.1.3
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 +160 -2
- package/dist/auth/lock.d.ts +14 -0
- package/dist/auth/lock.js +71 -0
- package/dist/auth/provider.d.ts +89 -0
- package/dist/auth/provider.js +884 -0
- package/dist/auth/store.d.ts +25 -0
- package/dist/auth/store.js +106 -0
- package/dist/auth/types.d.ts +138 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli/config.d.ts +26 -0
- package/dist/cli/config.js +98 -0
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +83 -0
- package/dist/cli/io.d.ts +6 -0
- package/dist/cli/io.js +61 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +275 -0
- package/dist/cli/sync.d.ts +4 -0
- package/dist/cli/sync.js +105 -0
- package/dist/connect/client.d.ts +32 -0
- package/dist/connect/client.js +257 -0
- package/dist/dto/result.d.ts +33 -0
- package/dist/dto/result.js +29 -0
- package/dist/dto/types.d.ts +63 -0
- package/dist/dto/types.js +2 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +64 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +11 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +82 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +408 -0
- package/dist/mcp/zod-json.d.ts +2 -0
- package/dist/mcp/zod-json.js +4 -0
- package/dist/ops/auth.d.ts +48 -0
- package/dist/ops/auth.js +89 -0
- package/dist/ops/bot.d.ts +52 -0
- package/dist/ops/bot.js +211 -0
- package/dist/ops/limits.d.ts +10 -0
- package/dist/ops/limits.js +31 -0
- package/dist/ops/map.d.ts +7 -0
- package/dist/ops/map.js +57 -0
- package/dist/ops/message.d.ts +33 -0
- package/dist/ops/message.js +118 -0
- package/dist/ops/plane.d.ts +26 -0
- package/dist/ops/plane.js +33 -0
- package/dist/ops/progress.d.ts +34 -0
- package/dist/ops/progress.js +92 -0
- package/dist/ops/session.d.ts +13 -0
- package/dist/ops/session.js +34 -0
- package/dist/ops/sidebar.d.ts +70 -0
- package/dist/ops/sidebar.js +205 -0
- package/dist/sync/attachments.d.ts +76 -0
- package/dist/sync/attachments.js +274 -0
- package/dist/sync/core.d.ts +92 -0
- package/dist/sync/core.js +1027 -0
- package/dist/sync/decode.d.ts +83 -0
- package/dist/sync/decode.js +229 -0
- package/dist/sync/download-policy.d.ts +3 -0
- package/dist/sync/download-policy.js +103 -0
- package/dist/sync/fs.d.ts +24 -0
- package/dist/sync/fs.js +252 -0
- package/dist/sync/hydrate.d.ts +43 -0
- package/dist/sync/hydrate.js +472 -0
- package/dist/sync/render.d.ts +5 -0
- package/dist/sync/render.js +100 -0
- package/dist/testing/mock-persist.d.ts +3 -0
- package/dist/testing/mock-persist.js +45 -0
- package/dist/testing/mock-transport.d.ts +18 -0
- package/dist/testing/mock-transport.js +202 -0
- package/docs/acceptance.md +19 -0
- package/docs/architecture.md +128 -0
- package/docs/auth-lock.md +87 -0
- package/docs/implementation-plan.md +14 -0
- package/docs/implementation-status.md +92 -0
- package/docs/proto-drift.md +9 -0
- package/docs/protocol/README.md +34 -0
- package/docs/protocol/attachment-pagination-0.79.3.md +32 -0
- package/docs/protocol/auth-flow.md +51 -0
- package/docs/protocol/baseline-manifest.md +72 -0
- package/docs/protocol/baselines/full-schema-diff-0.78.4-to-0.79.2.txt +239 -0
- package/docs/protocol/baselines/full-schema-diff-0.78.4-to-0.79.3.txt +240 -0
- package/docs/protocol/baselines/full-schema-diff-0.79.2-to-0.79.3.txt +23 -0
- package/docs/protocol/baselines/schema-0.79.3/README.md +7 -0
- package/docs/protocol/baselines/schema-0.79.3/agent.v1.proto +5184 -0
- package/docs/protocol/baselines/schema-0.79.3/aiserver.v1.proto +18182 -0
- package/docs/protocol/baselines/schema-0.79.3/aiserver.v1_2.proto +54 -0
- package/docs/protocol/baselines/schema-0.79.3/anyrun.v1.proto +37 -0
- package/docs/protocol/baselines/schema-0.79.3/internapi.v1.proto +35 -0
- package/docs/protocol/baselines/schema-0.79.3/snapshot.json.gz +0 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AgentStoreService.proto +34 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AiService.proto +17 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.AnalyticsService.proto +17 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.BackgroundComposerService.proto +19 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.DashboardService.proto +101 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.GrokBotService.proto +473 -0
- package/docs/protocol/baselines/schema-0.79.3/svc_aiserver.v1.SandBoxService.proto +64 -0
- package/docs/protocol/baselines/used-surface-0.78.4.json +3694 -0
- package/docs/protocol/baselines/used-surface-0.79.2.json +3835 -0
- package/docs/protocol/baselines/used-surface-0.79.3.json +3835 -0
- package/docs/protocol/baselines/used-surface-diff-0.78.4-to-0.79.2.md +34 -0
- package/docs/protocol/baselines/used-surface-diff-0.78.4-to-0.79.3.md +33 -0
- package/docs/protocol/compat-matrix.md +20 -0
- package/docs/protocol/static-restore.md +64 -0
- package/docs/protocol/surface-map.md +39 -0
- package/docs/protocol/transcript-body-0.79.3.md +98 -0
- package/docs/protocol/update-runbook.md +30 -0
- package/docs/protocol-map.md +106 -0
- package/docs/references.md +55 -0
- package/docs/requirements.md +26 -0
- package/docs/review.md +36 -0
- package/docs/sync-format.md +98 -0
- package/package.json +60 -4
- package/vendor/proto-gen/NOTICE.md +23 -0
- package/vendor/proto-gen/agent.v1_pb.js +9503 -0
- package/vendor/proto-gen/aiserver.v1_2_pb.js +105 -0
- package/vendor/proto-gen/aiserver.v1_pb.js +33163 -0
- package/vendor/proto-gen/anyrun.v1_pb.js +64 -0
- package/vendor/proto-gen/internapi.v1_pb.js +60 -0
- package/vendor/proto-gen/package.json +10 -0
- package/vendor/proto-gen/svc_aiserver.v1.GrokBotService_connect.js +4193 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mapConnectError } from "../connect/client.js";
|
|
2
|
+
import { failResult, newRequestId, okResult } from "../dto/result.js";
|
|
3
|
+
import { GbcError } from "../errors.js";
|
|
4
|
+
import { mapSession } from "./map.js";
|
|
5
|
+
export class SessionOps {
|
|
6
|
+
client;
|
|
7
|
+
flags;
|
|
8
|
+
constructor(client, flags = {}) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.flags = flags;
|
|
11
|
+
}
|
|
12
|
+
async list(agentId) {
|
|
13
|
+
const request_id = newRequestId();
|
|
14
|
+
try {
|
|
15
|
+
const res = await this.client.listGrokBotAgentSessions({ agentId });
|
|
16
|
+
return {
|
|
17
|
+
...okResult({ sessions: (res.sessions ?? []).map(mapSession) }, { request_id }),
|
|
18
|
+
simulated: this.flags.simulated,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
const err = e instanceof GbcError ? e : mapConnectError(e, request_id);
|
|
23
|
+
return {
|
|
24
|
+
...failResult(err.code, err.message, {
|
|
25
|
+
outcome: err.outcome,
|
|
26
|
+
retryable: err.retryable,
|
|
27
|
+
hint: err.hint,
|
|
28
|
+
request_id,
|
|
29
|
+
}),
|
|
30
|
+
simulated: this.flags.simulated,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { GrokBotClient } from "../connect/client.js";
|
|
2
|
+
import { type ResultEnvelope } from "../dto/result.js";
|
|
3
|
+
import type { SidebarSection } from "../dto/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* SidebarSections via Get/UpdateGrokBotUserRuntimeSettings.
|
|
6
|
+
* Only patches sidebar_sections (+ base). Not a cross-App transaction.
|
|
7
|
+
* Delete section ≠ delete bot. Room is separate (unsupported here).
|
|
8
|
+
*/
|
|
9
|
+
export declare class SidebarOps {
|
|
10
|
+
private client;
|
|
11
|
+
private flags;
|
|
12
|
+
private processLock;
|
|
13
|
+
constructor(client: GrokBotClient, flags?: {
|
|
14
|
+
simulated?: boolean;
|
|
15
|
+
});
|
|
16
|
+
private sim;
|
|
17
|
+
private locked;
|
|
18
|
+
list(): Promise<ResultEnvelope<{
|
|
19
|
+
sections: SidebarSection[];
|
|
20
|
+
note: string;
|
|
21
|
+
}>>;
|
|
22
|
+
patch(mutate: (sections: SidebarSection[]) => SidebarSection[], write?: {
|
|
23
|
+
dryRun?: boolean;
|
|
24
|
+
allowWrite?: boolean;
|
|
25
|
+
verify?: boolean;
|
|
26
|
+
}): Promise<ResultEnvelope<{
|
|
27
|
+
sections: SidebarSection[];
|
|
28
|
+
verified?: boolean;
|
|
29
|
+
note: string;
|
|
30
|
+
}>>;
|
|
31
|
+
createSection(name: string, write?: {
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
allowWrite?: boolean;
|
|
34
|
+
}): Promise<ResultEnvelope<{
|
|
35
|
+
sections: SidebarSection[];
|
|
36
|
+
verified?: boolean;
|
|
37
|
+
note: string;
|
|
38
|
+
}>>;
|
|
39
|
+
renameSection(sectionId: string, name: string, write?: {
|
|
40
|
+
dryRun?: boolean;
|
|
41
|
+
allowWrite?: boolean;
|
|
42
|
+
}): Promise<ResultEnvelope<{
|
|
43
|
+
sections: SidebarSection[];
|
|
44
|
+
verified?: boolean;
|
|
45
|
+
note: string;
|
|
46
|
+
}>>;
|
|
47
|
+
deleteSection(sectionId: string, write?: {
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
allowWrite?: boolean;
|
|
50
|
+
confirm?: string;
|
|
51
|
+
}): Promise<ResultEnvelope<{
|
|
52
|
+
sections: SidebarSection[];
|
|
53
|
+
verified?: boolean;
|
|
54
|
+
note: string;
|
|
55
|
+
}>>;
|
|
56
|
+
moveBot(agentId: string, toSectionId: string, write?: {
|
|
57
|
+
dryRun?: boolean;
|
|
58
|
+
allowWrite?: boolean;
|
|
59
|
+
}): Promise<ResultEnvelope<{
|
|
60
|
+
sections: SidebarSection[];
|
|
61
|
+
verified?: boolean;
|
|
62
|
+
note: string;
|
|
63
|
+
}>>;
|
|
64
|
+
private fromErr;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Semantic diff: section order (by id) and names are meaningful; agent_ids are
|
|
68
|
+
* compared as a set (in-section ordering changes are not treated as mismatch).
|
|
69
|
+
*/
|
|
70
|
+
export declare function diffSections(expected: SidebarSection[], actual: SidebarSection[]): string[];
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { mapConnectError } from "../connect/client.js";
|
|
2
|
+
import { failResult, newRequestId, okResult } from "../dto/result.js";
|
|
3
|
+
import { GbcError } from "../errors.js";
|
|
4
|
+
import { mapSection } from "./map.js";
|
|
5
|
+
/**
|
|
6
|
+
* SidebarSections via Get/UpdateGrokBotUserRuntimeSettings.
|
|
7
|
+
* Only patches sidebar_sections (+ base). Not a cross-App transaction.
|
|
8
|
+
* Delete section ≠ delete bot. Room is separate (unsupported here).
|
|
9
|
+
*/
|
|
10
|
+
export class SidebarOps {
|
|
11
|
+
client;
|
|
12
|
+
flags;
|
|
13
|
+
processLock = Promise.resolve();
|
|
14
|
+
constructor(client, flags = {}) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
this.flags = flags;
|
|
17
|
+
}
|
|
18
|
+
sim(r) {
|
|
19
|
+
if (this.flags.simulated)
|
|
20
|
+
r.simulated = true;
|
|
21
|
+
return r;
|
|
22
|
+
}
|
|
23
|
+
async locked(fn) {
|
|
24
|
+
let release;
|
|
25
|
+
const next = new Promise((r) => {
|
|
26
|
+
release = r;
|
|
27
|
+
});
|
|
28
|
+
const prev = this.processLock;
|
|
29
|
+
this.processLock = next;
|
|
30
|
+
await prev;
|
|
31
|
+
try {
|
|
32
|
+
return await fn();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
release();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async list() {
|
|
39
|
+
const request_id = newRequestId();
|
|
40
|
+
try {
|
|
41
|
+
const res = await this.client.getGrokBotUserRuntimeSettings({});
|
|
42
|
+
const sections = (res.settings?.sidebarSections?.sections ?? []).map(mapSection);
|
|
43
|
+
return this.sim(okResult({
|
|
44
|
+
sections,
|
|
45
|
+
note: "Read of SidebarSections only. Room/TeamGroup are not sidebar.",
|
|
46
|
+
}, { request_id }));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return this.fromErr(e, request_id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async patch(mutate, write) {
|
|
53
|
+
return this.locked(async () => {
|
|
54
|
+
const request_id = newRequestId();
|
|
55
|
+
try {
|
|
56
|
+
const current = await this.client.getGrokBotUserRuntimeSettings({});
|
|
57
|
+
const baseSections = (current.settings?.sidebarSections?.sections ?? []).map(mapSection);
|
|
58
|
+
const next = mutate(structuredClone(baseSections));
|
|
59
|
+
if (write?.dryRun) {
|
|
60
|
+
return this.sim(okResult({
|
|
61
|
+
sections: next,
|
|
62
|
+
note: "dry-run: would UpdateGrokBotUserRuntimeSettings with sections+base only",
|
|
63
|
+
}, { request_id, code: "dry_run" }));
|
|
64
|
+
}
|
|
65
|
+
if (!write?.allowWrite && process.env.GBT_ALLOW_WRITES !== "1") {
|
|
66
|
+
return this.sim(failResult("denied", "need --allow-writes for sidebar write", { request_id }));
|
|
67
|
+
}
|
|
68
|
+
let writeSent = false;
|
|
69
|
+
try {
|
|
70
|
+
writeSent = true;
|
|
71
|
+
await this.client.updateGrokBotUserRuntimeSettings({
|
|
72
|
+
sidebarSections: { sections: toWire(next) },
|
|
73
|
+
sidebarSectionsBase: { sections: toWire(baseSections) },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
const err = e instanceof GbcError ? e : mapConnectError(e, request_id, { write: true });
|
|
78
|
+
if (err.code === "failed_precondition" || /SidebarSectionsSaveConflict|conflict/i.test(err.message) && err.code !== "unknown") {
|
|
79
|
+
return this.sim(failResult("sidebar_conflict", err.message, {
|
|
80
|
+
request_id,
|
|
81
|
+
hint: "Re-list sidebar, re-apply minimal patch, write again. No strong cross-App guarantee.",
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return this.fromErr(err, request_id);
|
|
85
|
+
}
|
|
86
|
+
if (write?.verify === false) {
|
|
87
|
+
return this.sim(okResult({ sections: next, note: "Wrote sections+base; verify skipped" }, { request_id }));
|
|
88
|
+
}
|
|
89
|
+
let got;
|
|
90
|
+
try {
|
|
91
|
+
const again = await this.client.getGrokBotUserRuntimeSettings({});
|
|
92
|
+
got = (again.settings?.sidebarSections?.sections ?? []).map(mapSection);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
const err = e instanceof GbcError ? e : mapConnectError(e, request_id);
|
|
96
|
+
return this.sim(failResult("sidebar_unverified", `write sent but read-back failed: ${err.message}`, {
|
|
97
|
+
request_id,
|
|
98
|
+
outcome: "unknown",
|
|
99
|
+
hint: "Re-run `gbc sidebar list` and compare before retrying.",
|
|
100
|
+
details: { write_sent: writeSent, expected: next },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
const diff = diffSections(next, got);
|
|
104
|
+
if (diff.length > 0) {
|
|
105
|
+
return this.sim(failResult("sidebar_unverified", "sidebar read-back does not match the intended state", {
|
|
106
|
+
request_id,
|
|
107
|
+
outcome: "unknown",
|
|
108
|
+
hint: "Write may have been dropped or overwritten concurrently. Re-list and re-apply; do not blindly retry.",
|
|
109
|
+
details: { diff, expected: next, actual: got },
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return this.sim(okResult({
|
|
113
|
+
sections: got,
|
|
114
|
+
verified: true,
|
|
115
|
+
note: "Verified: section order, names and agent membership match (agent_ids order not compared). Best-effort base CAS only; not cross-App transactional.",
|
|
116
|
+
}, { request_id }));
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return this.fromErr(e, request_id);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
createSection(name, write) {
|
|
124
|
+
return this.patch((sections) => {
|
|
125
|
+
sections.push({ id: crypto.randomUUID(), name, agent_ids: [] });
|
|
126
|
+
return sections;
|
|
127
|
+
}, write);
|
|
128
|
+
}
|
|
129
|
+
renameSection(sectionId, name, write) {
|
|
130
|
+
return this.patch((sections) => {
|
|
131
|
+
const s = sections.find((x) => x.id === sectionId);
|
|
132
|
+
if (!s)
|
|
133
|
+
throw new GbcError("not_found", `section ${sectionId} not found`, { exitCode: 5 });
|
|
134
|
+
s.name = name;
|
|
135
|
+
return sections;
|
|
136
|
+
}, write);
|
|
137
|
+
}
|
|
138
|
+
deleteSection(sectionId, write) {
|
|
139
|
+
if (!write?.dryRun && write?.confirm !== sectionId) {
|
|
140
|
+
return Promise.resolve(this.sim(failResult("denied", "delete section requires --confirm=<section_id> matching --section-id", {
|
|
141
|
+
request_id: newRequestId(),
|
|
142
|
+
hint: `gbc sidebar delete --section-id ${sectionId} --confirm=${sectionId} --allow-writes`,
|
|
143
|
+
})));
|
|
144
|
+
}
|
|
145
|
+
return this.patch((sections) => {
|
|
146
|
+
const next = sections.filter((x) => x.id !== sectionId);
|
|
147
|
+
if (next.length === sections.length) {
|
|
148
|
+
throw new GbcError("not_found", `section ${sectionId} not found`, { exitCode: 5 });
|
|
149
|
+
}
|
|
150
|
+
return next;
|
|
151
|
+
}, write);
|
|
152
|
+
}
|
|
153
|
+
moveBot(agentId, toSectionId, write) {
|
|
154
|
+
return this.patch((sections) => {
|
|
155
|
+
for (const s of sections) {
|
|
156
|
+
s.agent_ids = s.agent_ids.filter((id) => id !== agentId);
|
|
157
|
+
}
|
|
158
|
+
const dest = sections.find((s) => s.id === toSectionId);
|
|
159
|
+
if (!dest)
|
|
160
|
+
throw new GbcError("not_found", `section ${toSectionId} not found`, { exitCode: 5 });
|
|
161
|
+
dest.agent_ids.push(agentId);
|
|
162
|
+
return sections;
|
|
163
|
+
}, write);
|
|
164
|
+
}
|
|
165
|
+
fromErr(e, request_id) {
|
|
166
|
+
const err = e instanceof GbcError ? e : mapConnectError(e, request_id);
|
|
167
|
+
return this.sim(failResult(err.code, err.message, {
|
|
168
|
+
outcome: err.outcome,
|
|
169
|
+
retryable: err.retryable,
|
|
170
|
+
hint: err.hint,
|
|
171
|
+
request_id,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function toWire(sections) {
|
|
176
|
+
return sections.map((s) => ({ id: s.id, name: s.name, agentIds: s.agent_ids }));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Semantic diff: section order (by id) and names are meaningful; agent_ids are
|
|
180
|
+
* compared as a set (in-section ordering changes are not treated as mismatch).
|
|
181
|
+
*/
|
|
182
|
+
export function diffSections(expected, actual) {
|
|
183
|
+
const out = [];
|
|
184
|
+
const eIds = expected.map((s) => s.id);
|
|
185
|
+
const aIds = actual.map((s) => s.id);
|
|
186
|
+
if (eIds.join("\u0000") !== aIds.join("\u0000")) {
|
|
187
|
+
out.push(`section order/ids differ: expected [${eIds.join(",")}] got [${aIds.join(",")}]`);
|
|
188
|
+
}
|
|
189
|
+
const byId = new Map(actual.map((s) => [s.id, s]));
|
|
190
|
+
for (const e of expected) {
|
|
191
|
+
const a = byId.get(e.id);
|
|
192
|
+
if (!a)
|
|
193
|
+
continue;
|
|
194
|
+
if (a.name !== e.name)
|
|
195
|
+
out.push(`section ${e.id} name: expected "${e.name}" got "${a.name}"`);
|
|
196
|
+
const want = new Set(e.agent_ids);
|
|
197
|
+
const have = new Set(a.agent_ids);
|
|
198
|
+
const missing = [...want].filter((x) => !have.has(x));
|
|
199
|
+
const extra = [...have].filter((x) => !want.has(x));
|
|
200
|
+
if (missing.length || extra.length) {
|
|
201
|
+
out.push(`section ${e.id} agents: missing [${missing.join(",")}] extra [${extra.join(",")}]`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare const ATTACHMENT_DOWNLOADER = "resolve-attachment-reads/0.79.3-ondemand-v1";
|
|
2
|
+
export declare const DEFAULT_ATTACH: {
|
|
3
|
+
readonly maxFileBytes: number;
|
|
4
|
+
readonly maxBytes: number;
|
|
5
|
+
readonly timeoutMs: 30000;
|
|
6
|
+
readonly resolveBatch: 8;
|
|
7
|
+
};
|
|
8
|
+
export declare const HARD_ATTACH: {
|
|
9
|
+
readonly maxFileBytes: number;
|
|
10
|
+
readonly maxBytes: number;
|
|
11
|
+
readonly timeoutMs: 300000;
|
|
12
|
+
readonly resolveBatch: 32;
|
|
13
|
+
readonly pathBytes: 4096;
|
|
14
|
+
};
|
|
15
|
+
export type AttachmentLocalStatus = "ok" | "not_in_store" | "no_storage" | "error" | "skipped";
|
|
16
|
+
export type AttachmentLocal = {
|
|
17
|
+
remote_path: string;
|
|
18
|
+
status: AttachmentLocalStatus;
|
|
19
|
+
relative_path?: string;
|
|
20
|
+
content_sha256?: string;
|
|
21
|
+
size_bytes?: number;
|
|
22
|
+
content_type?: string;
|
|
23
|
+
etag?: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
observed_at_ms: string;
|
|
26
|
+
live_verified: false;
|
|
27
|
+
};
|
|
28
|
+
export declare function assertRemotePath(value: string): string;
|
|
29
|
+
export declare function pathKey(remotePath: string): string;
|
|
30
|
+
export declare function objectRel(agentId: string, sha256: string): string;
|
|
31
|
+
export declare function refRel(agentId: string, remotePath: string, sessionId?: string): string;
|
|
32
|
+
export declare function mapOutcome(outcome: unknown): AttachmentLocalStatus | "store";
|
|
33
|
+
export type ResolvedStore = {
|
|
34
|
+
remote_path: string;
|
|
35
|
+
url: string;
|
|
36
|
+
expires_at_ms?: number;
|
|
37
|
+
content_type?: string;
|
|
38
|
+
size_bytes?: number;
|
|
39
|
+
etag?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Validate STORE read without exposing the URL to callers' logs. */
|
|
42
|
+
export declare function acceptStoreRead(remotePath: string, read: any, nowMs: number): {
|
|
43
|
+
ok: true;
|
|
44
|
+
store: ResolvedStore;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
status: AttachmentLocalStatus;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
export type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
51
|
+
export type TransferBudget = {
|
|
52
|
+
bytes: number;
|
|
53
|
+
maxBytes: number;
|
|
54
|
+
deadline: number;
|
|
55
|
+
};
|
|
56
|
+
/** Stream directly into a private staging sink. Failed network bytes count too. */
|
|
57
|
+
export declare function downloadToStage(url: string, opts: {
|
|
58
|
+
maxBytes: number;
|
|
59
|
+
expectedSize?: number;
|
|
60
|
+
budget: TransferBudget;
|
|
61
|
+
fetchImpl: FetchFn;
|
|
62
|
+
etag?: string;
|
|
63
|
+
sink: {
|
|
64
|
+
write(bytes: Uint8Array): void;
|
|
65
|
+
discard(): void;
|
|
66
|
+
};
|
|
67
|
+
}): Promise<{
|
|
68
|
+
status: number;
|
|
69
|
+
digest?: string;
|
|
70
|
+
size?: number;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function sha256Hex(data: Uint8Array): string;
|
|
73
|
+
export declare function redactAttachmentError(e: unknown): {
|
|
74
|
+
status: AttachmentLocalStatus;
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/** On-demand user-attachment download (App 0.79.3 ResolveGrokBotAttachmentReads evidence).
|
|
2
|
+
* Not transcript blob_hash hydration. Signed URLs are never persisted or echoed.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { GbcError } from "../errors.js";
|
|
6
|
+
import { component } from "./fs.js";
|
|
7
|
+
export const ATTACHMENT_DOWNLOADER = "resolve-attachment-reads/0.79.3-ondemand-v1";
|
|
8
|
+
export const DEFAULT_ATTACH = {
|
|
9
|
+
maxFileBytes: 32 * 1024 * 1024,
|
|
10
|
+
maxBytes: 64 * 1024 * 1024,
|
|
11
|
+
timeoutMs: 30000,
|
|
12
|
+
resolveBatch: 8,
|
|
13
|
+
};
|
|
14
|
+
export const HARD_ATTACH = {
|
|
15
|
+
maxFileBytes: 128 * 1024 * 1024,
|
|
16
|
+
maxBytes: 256 * 1024 * 1024,
|
|
17
|
+
timeoutMs: 300000,
|
|
18
|
+
resolveBatch: 32,
|
|
19
|
+
pathBytes: 4096,
|
|
20
|
+
};
|
|
21
|
+
// App main-core: /home/box/agent-data (model alias), /home/box/sand-data.
|
|
22
|
+
// These strings are REMOTE references, never paths read from this computer.
|
|
23
|
+
export function assertRemotePath(value) {
|
|
24
|
+
if (typeof value !== "string" ||
|
|
25
|
+
!value ||
|
|
26
|
+
Buffer.byteLength(value) > 4096 ||
|
|
27
|
+
/[\x00-\x1f\x7f\\]/.test(value))
|
|
28
|
+
throw new GbcError("invalid_argument", "invalid remote attachment reference");
|
|
29
|
+
let p = value;
|
|
30
|
+
if (p.startsWith("file:")) {
|
|
31
|
+
let u;
|
|
32
|
+
try {
|
|
33
|
+
u = new URL(p);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new GbcError("invalid_argument", "invalid remote file URL");
|
|
37
|
+
}
|
|
38
|
+
if (u.host ||
|
|
39
|
+
u.search ||
|
|
40
|
+
u.hash ||
|
|
41
|
+
p !== `file://${u.pathname}` ||
|
|
42
|
+
/%2f|%5c/i.test(u.pathname))
|
|
43
|
+
throw new GbcError("invalid_argument", "invalid remote file URL");
|
|
44
|
+
try {
|
|
45
|
+
p = decodeURIComponent(u.pathname);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new GbcError("invalid_argument", "invalid remote file URL encoding");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (/[\x00-\x1f\x7f\\]/.test(p) ||
|
|
52
|
+
p.split("/").some((x) => x === "." || x === ".."))
|
|
53
|
+
throw new GbcError("invalid_argument", "unsafe remote attachment reference");
|
|
54
|
+
const m = /^\/home\/box\/(?:agent-data|sand-data)\/agents\/([A-Za-z0-9._-]{1,128})\/(attachments|assets)\/([0-9a-f]{64}\.[0-9a-z]{1,16})$/.exec(p);
|
|
55
|
+
if (!m || m[1] === "." || m[1] === "..")
|
|
56
|
+
throw new GbcError("unsupported", "not a supported App store attachment alias");
|
|
57
|
+
return `/home/box/agent-data/agents/${m[1]}/${m[2]}/${m[3]}`;
|
|
58
|
+
}
|
|
59
|
+
export function pathKey(remotePath) {
|
|
60
|
+
return createHash("sha256")
|
|
61
|
+
.update(assertRemotePath(remotePath))
|
|
62
|
+
.digest("hex");
|
|
63
|
+
}
|
|
64
|
+
export function objectRel(agentId, sha256) {
|
|
65
|
+
if (!/^[0-9a-f]{64}$/.test(sha256))
|
|
66
|
+
throw new GbcError("invalid_argument", "invalid content hash");
|
|
67
|
+
return `bots/${component(agentId)}/attachments/objects/sha256-${sha256}`;
|
|
68
|
+
}
|
|
69
|
+
export function refRel(agentId, remotePath, sessionId = "") {
|
|
70
|
+
const sk = createHash("sha256").update(sessionId).digest("hex");
|
|
71
|
+
return `bots/${component(agentId)}/attachments/refs-v2/${sk}/${pathKey(remotePath)}.json`;
|
|
72
|
+
}
|
|
73
|
+
export function mapOutcome(outcome) {
|
|
74
|
+
const n = typeof outcome === "number"
|
|
75
|
+
? outcome
|
|
76
|
+
: typeof outcome === "bigint"
|
|
77
|
+
? Number(outcome)
|
|
78
|
+
: undefined;
|
|
79
|
+
const s = typeof outcome === "string" ? outcome : undefined;
|
|
80
|
+
if (n === 1 ||
|
|
81
|
+
s === "STORE" ||
|
|
82
|
+
s === "GROK_BOT_ATTACHMENT_READ_OUTCOME_STORE")
|
|
83
|
+
return "store";
|
|
84
|
+
if (n === 2 ||
|
|
85
|
+
s === "NOT_IN_STORE" ||
|
|
86
|
+
s === "GROK_BOT_ATTACHMENT_READ_OUTCOME_NOT_IN_STORE")
|
|
87
|
+
return "not_in_store";
|
|
88
|
+
if (n === 3 ||
|
|
89
|
+
s === "NO_STORAGE" ||
|
|
90
|
+
s === "GROK_BOT_ATTACHMENT_READ_OUTCOME_NO_STORAGE")
|
|
91
|
+
return "no_storage";
|
|
92
|
+
return "error";
|
|
93
|
+
}
|
|
94
|
+
function int64Ms(v) {
|
|
95
|
+
if (v === undefined || v === null || v === "")
|
|
96
|
+
return undefined;
|
|
97
|
+
if (typeof v === "bigint") {
|
|
98
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < 0n)
|
|
99
|
+
return undefined;
|
|
100
|
+
return Number(v);
|
|
101
|
+
}
|
|
102
|
+
if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0)
|
|
103
|
+
return v;
|
|
104
|
+
if (typeof v === "string" && /^\d+$/.test(v)) {
|
|
105
|
+
const n = Number(v);
|
|
106
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function int64Size(v) {
|
|
111
|
+
return int64Ms(v);
|
|
112
|
+
}
|
|
113
|
+
/** Validate STORE read without exposing the URL to callers' logs. */
|
|
114
|
+
export function acceptStoreRead(remotePath, read, nowMs) {
|
|
115
|
+
const path = String(read?.path ?? "");
|
|
116
|
+
if (path !== remotePath)
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
status: "error",
|
|
120
|
+
reason: "resolve_path_mismatch",
|
|
121
|
+
};
|
|
122
|
+
const mapped = mapOutcome(read?.outcome);
|
|
123
|
+
if (mapped !== "store")
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
status: mapped === "error" ? "error" : mapped,
|
|
127
|
+
reason: mapped === "error" ? "resolve_outcome_unusable" : mapped,
|
|
128
|
+
};
|
|
129
|
+
const url = read?.url;
|
|
130
|
+
if (typeof url !== "string" || !url)
|
|
131
|
+
return { ok: false, status: "error", reason: "store_missing_url" };
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = new URL(url);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { ok: false, status: "error", reason: "store_url_unusable" };
|
|
138
|
+
}
|
|
139
|
+
if (parsed.protocol !== "https:" ||
|
|
140
|
+
parsed.username ||
|
|
141
|
+
parsed.password ||
|
|
142
|
+
parsed.hash ||
|
|
143
|
+
(parsed.port && parsed.port !== "443"))
|
|
144
|
+
return { ok: false, status: "error", reason: "store_url_not_https" };
|
|
145
|
+
const expires = int64Ms(read?.expiresAtMs ?? read?.expires_at_ms);
|
|
146
|
+
if (expires === undefined || expires <= nowMs + 1000)
|
|
147
|
+
return { ok: false, status: "error", reason: "store_url_expired" };
|
|
148
|
+
const size = int64Size(read?.sizeBytes ?? read?.size_bytes);
|
|
149
|
+
const contentType = typeof read?.contentType === "string"
|
|
150
|
+
? read.contentType
|
|
151
|
+
: typeof read?.content_type === "string"
|
|
152
|
+
? read.content_type
|
|
153
|
+
: undefined;
|
|
154
|
+
const etag = typeof read?.etag === "string" ? read.etag : undefined;
|
|
155
|
+
if (size === undefined ||
|
|
156
|
+
!etag ||
|
|
157
|
+
etag.length > 512 ||
|
|
158
|
+
/[\x00-\x20\x7f]/.test(etag))
|
|
159
|
+
return { ok: false, status: "error", reason: "invalid_store_metadata" };
|
|
160
|
+
if (contentType &&
|
|
161
|
+
(contentType.length > 256 || /[\x00-\x1f\x7f]/.test(contentType)))
|
|
162
|
+
return { ok: false, status: "error", reason: "invalid_store_metadata" };
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
store: {
|
|
166
|
+
remote_path: remotePath,
|
|
167
|
+
url,
|
|
168
|
+
expires_at_ms: expires,
|
|
169
|
+
content_type: contentType,
|
|
170
|
+
size_bytes: size,
|
|
171
|
+
etag,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Stream directly into a private staging sink. Failed network bytes count too. */
|
|
176
|
+
export async function downloadToStage(url, opts) {
|
|
177
|
+
const remain = opts.budget.deadline - Date.now();
|
|
178
|
+
if (remain <= 0)
|
|
179
|
+
throw new GbcError("deadline_exceeded", "attachment command deadline");
|
|
180
|
+
if (opts.expectedSize !== undefined &&
|
|
181
|
+
(opts.expectedSize > opts.maxBytes ||
|
|
182
|
+
opts.expectedSize > opts.budget.maxBytes - opts.budget.bytes))
|
|
183
|
+
throw new GbcError("resource_exhausted", "attachment size exceeds remaining budget");
|
|
184
|
+
const ac = new AbortController(), timer = setTimeout(() => ac.abort(), remain);
|
|
185
|
+
let reader;
|
|
186
|
+
let response;
|
|
187
|
+
const aborted = new Promise((_, reject) => ac.signal.addEventListener("abort", () => reject(new GbcError("deadline_exceeded", "attachment deadline")), { once: true }));
|
|
188
|
+
try {
|
|
189
|
+
const headers = {};
|
|
190
|
+
if (opts.etag)
|
|
191
|
+
headers["if-none-match"] =
|
|
192
|
+
opts.etag.startsWith('"') || opts.etag.startsWith("W/")
|
|
193
|
+
? opts.etag
|
|
194
|
+
: `"${opts.etag}"`;
|
|
195
|
+
const res = await Promise.race([
|
|
196
|
+
opts.fetchImpl(url, {
|
|
197
|
+
method: "GET",
|
|
198
|
+
headers,
|
|
199
|
+
redirect: "error",
|
|
200
|
+
signal: ac.signal,
|
|
201
|
+
}),
|
|
202
|
+
aborted,
|
|
203
|
+
]);
|
|
204
|
+
response = res;
|
|
205
|
+
if (res.status === 403 || (res.status === 304 && opts.etag)) {
|
|
206
|
+
await res.body?.cancel();
|
|
207
|
+
return { status: res.status };
|
|
208
|
+
}
|
|
209
|
+
if (res.status !== 200)
|
|
210
|
+
throw new GbcError("unavailable", "attachment HTTP response rejected");
|
|
211
|
+
const len = res.headers.get("content-length");
|
|
212
|
+
if (len !== null &&
|
|
213
|
+
(!/^\d+$/.test(len) ||
|
|
214
|
+
!Number.isSafeInteger(Number(len)) ||
|
|
215
|
+
Number(len) > opts.maxBytes ||
|
|
216
|
+
Number(len) > opts.budget.maxBytes - opts.budget.bytes ||
|
|
217
|
+
(opts.expectedSize !== undefined && Number(len) !== opts.expectedSize)))
|
|
218
|
+
throw new GbcError("failed_precondition", "attachment size header mismatch");
|
|
219
|
+
const h = createHash("sha256");
|
|
220
|
+
let size = 0;
|
|
221
|
+
reader = res.body?.getReader();
|
|
222
|
+
if (!reader && opts.expectedSize !== 0)
|
|
223
|
+
throw new GbcError("failed_precondition", "attachment body missing");
|
|
224
|
+
while (reader) {
|
|
225
|
+
if (Date.now() >= opts.budget.deadline)
|
|
226
|
+
throw new GbcError("deadline_exceeded", "attachment command deadline");
|
|
227
|
+
const { value, done } = await Promise.race([reader.read(), aborted]);
|
|
228
|
+
if (done)
|
|
229
|
+
break;
|
|
230
|
+
size += value.byteLength;
|
|
231
|
+
opts.budget.bytes += value.byteLength;
|
|
232
|
+
if (size > opts.maxBytes ||
|
|
233
|
+
(opts.expectedSize !== undefined && size > opts.expectedSize) ||
|
|
234
|
+
opts.budget.bytes > opts.budget.maxBytes)
|
|
235
|
+
throw new GbcError("resource_exhausted", "attachment streaming budget exceeded");
|
|
236
|
+
h.update(value);
|
|
237
|
+
opts.sink.write(value);
|
|
238
|
+
}
|
|
239
|
+
if ((opts.expectedSize !== undefined && size !== opts.expectedSize) ||
|
|
240
|
+
(len !== null && size !== Number(len)))
|
|
241
|
+
throw new GbcError("failed_precondition", "attachment size mismatch");
|
|
242
|
+
return { status: 200, digest: h.digest("hex"), size };
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
if (e instanceof GbcError)
|
|
246
|
+
throw e;
|
|
247
|
+
throw new GbcError(ac.signal.aborted ? "deadline_exceeded" : "unavailable", "attachment transfer failed (URL suppressed)");
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
if (reader) {
|
|
252
|
+
void reader.cancel().catch(() => { });
|
|
253
|
+
}
|
|
254
|
+
else if (response?.body) {
|
|
255
|
+
void response.body.cancel().catch(() => { });
|
|
256
|
+
}
|
|
257
|
+
// Do not reject the already-finished abort promise in finally.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export function sha256Hex(data) {
|
|
261
|
+
return createHash("sha256").update(data).digest("hex");
|
|
262
|
+
}
|
|
263
|
+
export function redactAttachmentError(e) {
|
|
264
|
+
if (e instanceof GbcError) {
|
|
265
|
+
if (e.code === "resource_exhausted")
|
|
266
|
+
return { status: "error", reason: "size_or_budget" };
|
|
267
|
+
if (e.code === "deadline_exceeded")
|
|
268
|
+
return { status: "error", reason: "timeout" };
|
|
269
|
+
if (e.code === "unavailable")
|
|
270
|
+
return { status: "error", reason: "http_or_transport" };
|
|
271
|
+
return { status: "error", reason: e.code };
|
|
272
|
+
}
|
|
273
|
+
return { status: "error", reason: "download_failed" };
|
|
274
|
+
}
|