totemlayer 0.1.20 → 0.1.24
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 +31 -29
- package/bin/agent-profile.mjs +13 -0
- package/bin/agent-runner.mjs +31 -2
- package/bin/agent-setup.mjs +58 -6
- package/bin/automatic-runtime.mjs +44 -6
- package/bin/bound-login.mjs +107 -0
- package/bin/codex-chat-bridge.mjs +197 -0
- package/bin/codex-desktop-wake.mjs +22 -0
- package/bin/connected-agent-service.mjs +115 -78
- package/bin/device-login.mjs +100 -8
- package/bin/executor-readiness.mjs +97 -20
- package/bin/installation.mjs +129 -74
- package/bin/mission-agent-service.mjs +23 -5
- package/bin/mission-coordination.mjs +311 -0
- package/bin/mission-worker.mjs +271 -20
- package/bin/model-adapter.mjs +30 -8
- package/bin/network-runtime.mjs +115 -6
- package/bin/runtime-diagnostics.mjs +4 -0
- package/bin/runtime-installation.mjs +1 -0
- package/bin/sdk/action-types.d.ts +236 -0
- package/bin/sdk/actions.d.ts +19 -0
- package/bin/sdk/errors.d.ts +30 -0
- package/bin/sdk/errors.js +14 -1
- package/bin/sdk/index.d.ts +298 -0
- package/bin/sdk/index.js +55 -11
- package/bin/sdk/inspector.d.ts +40 -0
- package/bin/sdk/request-options.d.ts +13 -0
- package/bin/sdk/transport.d.ts +24 -0
- package/bin/sdk/types.d.ts +638 -0
- package/bin/totem.mjs +2 -2
- package/package.json +1 -1
- package/scripts/prepare.mjs +9 -20
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createLocalInspector, InspectionError } from "./inspector.js";
|
|
2
|
+
import type { ActionProposal, AssistantActionCheck, AssistantActionCheckList, AssistantActionReceiptResult, AssistantCheckOptions } from "./action-types.js";
|
|
3
|
+
import type { MissionCoordinationUpdate, MissionSessionReport, Agent, AssistantContext, AssistantMissionPermissions, Attachment, Chat, ChatCommand, ChatDetail, ChatEvent, ChatRun, RuntimeHeartbeat, Conversation, ListenOptions, ListMessages, ListMissions, ListTasks, Message, MessageContent, MessageHandler, Metadata, MissionArtifact, MissionDetail, MissionEvent, MissionEventInput, MissionSummary, MissionWorkItem, Page, Pagination, RequestOptions, RepoClaim, RepoClaimState, RepositoryCoordination, SearchAgents, SendMessage, Subscription, Task, TaskState, TotemOptions, WriteOptions } from "./types.js";
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export * from "./actions.js";
|
|
7
|
+
export { idempotencyKey } from "./transport.js";
|
|
8
|
+
export declare class Totem {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(options: TotemOptions);
|
|
11
|
+
static connect(options: TotemOptions): Promise<ConnectedAgent>;
|
|
12
|
+
connect(): Promise<ConnectedAgent>;
|
|
13
|
+
agent(options?: RequestOptions): Promise<Agent>;
|
|
14
|
+
assistant: {
|
|
15
|
+
context: (options?: RequestOptions) => Promise<AssistantContext>;
|
|
16
|
+
checks: {
|
|
17
|
+
create: (missionId: string, input: {
|
|
18
|
+
workItemId: string;
|
|
19
|
+
leaseToken: string;
|
|
20
|
+
action: ActionProposal;
|
|
21
|
+
}, options: AssistantCheckOptions) => Promise<AssistantActionCheck>;
|
|
22
|
+
list: (missionId: string, workItemId: string, options?: RequestOptions) => Promise<AssistantActionCheckList>;
|
|
23
|
+
recordReceipt: (missionId: string, suiteId: string, input: {
|
|
24
|
+
workItemId: string;
|
|
25
|
+
}, options?: RequestOptions) => Promise<AssistantActionReceiptResult>;
|
|
26
|
+
};
|
|
27
|
+
permissions: (missionId: string, options?: RequestOptions) => Promise<AssistantMissionPermissions>;
|
|
28
|
+
};
|
|
29
|
+
resolve(address: string, options?: RequestOptions): Promise<Agent>;
|
|
30
|
+
agents: {
|
|
31
|
+
search: (query?: SearchAgents, options?: RequestOptions) => Promise<Page<Agent>>;
|
|
32
|
+
iterate: (query?: SearchAgents, options?: RequestOptions) => AsyncIterable<Agent>;
|
|
33
|
+
};
|
|
34
|
+
attachments: {
|
|
35
|
+
upload: (name: string, mediaType: string, bytes: Uint8Array, options?: RequestOptions) => Promise<Attachment>;
|
|
36
|
+
download: (id: string, options?: RequestOptions) => Promise<{
|
|
37
|
+
bytes: Uint8Array;
|
|
38
|
+
mediaType: string;
|
|
39
|
+
name: string;
|
|
40
|
+
sha256: string | null;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
messages: {
|
|
44
|
+
send: (input: SendMessage, options?: WriteOptions) => Promise<Message>;
|
|
45
|
+
list: (query?: ListMessages, options?: RequestOptions) => Promise<Page<Message>>;
|
|
46
|
+
iterate: (query?: ListMessages, options?: RequestOptions) => AsyncIterable<Message>;
|
|
47
|
+
get: (id: string, options?: RequestOptions) => Promise<Message>;
|
|
48
|
+
reply: (id: string, text: string, options?: WriteOptions & {
|
|
49
|
+
metadata?: Metadata;
|
|
50
|
+
attachments?: string[];
|
|
51
|
+
}) => Promise<Message>;
|
|
52
|
+
replyContent: (id: string, content: MessageContent, options?: WriteOptions) => Promise<Message>;
|
|
53
|
+
ack: (id: string, options?: RequestOptions) => Promise<Message>;
|
|
54
|
+
};
|
|
55
|
+
send(input: SendMessage, options?: WriteOptions): Promise<Message>;
|
|
56
|
+
send(to: string, text: string, options?: WriteOptions): Promise<Message>;
|
|
57
|
+
conversations: {
|
|
58
|
+
create: (input: {
|
|
59
|
+
with: string;
|
|
60
|
+
metadata?: Metadata;
|
|
61
|
+
}, options?: RequestOptions) => Promise<ConversationHandle>;
|
|
62
|
+
list: (query?: Pagination, options?: RequestOptions) => Promise<Page<Conversation>>;
|
|
63
|
+
get: (id: string, options?: RequestOptions) => Promise<Conversation>;
|
|
64
|
+
messages: (id: string, query?: Pagination, options?: RequestOptions) => Promise<Page<Message>>;
|
|
65
|
+
};
|
|
66
|
+
startConversation(input: {
|
|
67
|
+
with: string;
|
|
68
|
+
metadata?: Metadata;
|
|
69
|
+
}, options?: RequestOptions): Promise<ConversationHandle>;
|
|
70
|
+
missions: {
|
|
71
|
+
list: (options?: RequestOptions) => Promise<MissionSummary[]>;
|
|
72
|
+
listPage: (query?: ListMissions, options?: RequestOptions) => Promise<Page<MissionSummary>>;
|
|
73
|
+
iterate: (query?: ListMissions, options?: RequestOptions) => AsyncIterable<MissionSummary>;
|
|
74
|
+
get: (id: string, options?: RequestOptions) => Promise<MissionDetail>;
|
|
75
|
+
coordination: (id: string, version?: string, options?: RequestOptions) => Promise<MissionCoordinationUpdate>;
|
|
76
|
+
sessions: {
|
|
77
|
+
report: (id: string, input: MissionSessionReport, options?: RequestOptions) => Promise<{
|
|
78
|
+
sessionId: string;
|
|
79
|
+
sequence: number;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
workItems: (id: string, options?: RequestOptions) => Promise<MissionWorkItem[]>;
|
|
83
|
+
/** Empty data can still have a nextCursor: readiness is checked after scanning candidates. */
|
|
84
|
+
workItemsPage: (id: string, query?: Pagination, options?: RequestOptions) => Promise<Page<MissionWorkItem>>;
|
|
85
|
+
iterateWorkItems: (id: string, query?: Pagination, options?: RequestOptions) => AsyncIterable<MissionWorkItem>;
|
|
86
|
+
assign: (id: string, input: {
|
|
87
|
+
ownerMemberId: string;
|
|
88
|
+
objective: string;
|
|
89
|
+
requiredOutput: string;
|
|
90
|
+
dependsOnIds?: string[];
|
|
91
|
+
parentWorkItemId: string;
|
|
92
|
+
metadata?: Metadata;
|
|
93
|
+
}, options?: WriteOptions) => Promise<MissionWorkItem>;
|
|
94
|
+
claim: (id: string, workItemId: string, options?: WriteOptions) => Promise<{
|
|
95
|
+
workItem: MissionWorkItem;
|
|
96
|
+
leaseToken: string;
|
|
97
|
+
leaseExpiresAt: string;
|
|
98
|
+
}>;
|
|
99
|
+
renew: (id: string, workItemId: string, leaseToken: string, options?: RequestOptions) => Promise<{
|
|
100
|
+
workItem: MissionWorkItem;
|
|
101
|
+
leaseExpiresAt: string;
|
|
102
|
+
}>;
|
|
103
|
+
complete: (id: string, workItemId: string, input: {
|
|
104
|
+
leaseToken: string;
|
|
105
|
+
result?: Metadata;
|
|
106
|
+
artifact?: Pick<MissionArtifact, "kind" | "title"> & {
|
|
107
|
+
summary?: string;
|
|
108
|
+
uri?: string;
|
|
109
|
+
contentText?: string;
|
|
110
|
+
metadata?: Metadata;
|
|
111
|
+
};
|
|
112
|
+
event?: MissionEventInput;
|
|
113
|
+
}, options?: WriteOptions) => Promise<{
|
|
114
|
+
workItem: MissionWorkItem;
|
|
115
|
+
artifact?: MissionArtifact;
|
|
116
|
+
}>;
|
|
117
|
+
reserveEffect: (id: string, input: {
|
|
118
|
+
workItemId: string;
|
|
119
|
+
action: string;
|
|
120
|
+
resource: string;
|
|
121
|
+
leaseToken: string;
|
|
122
|
+
estimatedCostCents?: number;
|
|
123
|
+
parameters?: Metadata;
|
|
124
|
+
}, options?: WriteOptions) => Promise<{
|
|
125
|
+
id: string;
|
|
126
|
+
state: string;
|
|
127
|
+
action: string;
|
|
128
|
+
resource: string;
|
|
129
|
+
}>;
|
|
130
|
+
listEffects: (id: string, workItemId: string, options?: RequestOptions) => Promise<Array<{
|
|
131
|
+
id: string;
|
|
132
|
+
workItemId: string;
|
|
133
|
+
action: string;
|
|
134
|
+
resource: string;
|
|
135
|
+
state: string;
|
|
136
|
+
estimatedCostCents: number;
|
|
137
|
+
actualCostCents: number | null;
|
|
138
|
+
parameters?: Metadata | null;
|
|
139
|
+
receipt?: Metadata | null;
|
|
140
|
+
externalRef?: string | null;
|
|
141
|
+
createdAt: string;
|
|
142
|
+
updatedAt: string;
|
|
143
|
+
}>>;
|
|
144
|
+
dispatchEffect: (id: string, effectId: string, leaseToken: string, options?: RequestOptions) => Promise<{
|
|
145
|
+
effect: {
|
|
146
|
+
id: string;
|
|
147
|
+
state: string;
|
|
148
|
+
};
|
|
149
|
+
launchPermitted: true;
|
|
150
|
+
}>;
|
|
151
|
+
settleEffect: (id: string, effectId: string, input: {
|
|
152
|
+
state: "succeeded" | "failed" | "uncertain";
|
|
153
|
+
actualCostCents?: number;
|
|
154
|
+
receipt?: Metadata;
|
|
155
|
+
}, options?: RequestOptions) => Promise<{
|
|
156
|
+
id: string;
|
|
157
|
+
state: string;
|
|
158
|
+
}>;
|
|
159
|
+
events: {
|
|
160
|
+
list: (id: string, query?: Pagination, options?: RequestOptions) => Promise<Page<MissionEvent>>;
|
|
161
|
+
iterate: (id: string, query?: Pagination, options?: RequestOptions) => AsyncIterable<MissionEvent>;
|
|
162
|
+
post: (id: string, input: MissionEventInput, options?: WriteOptions) => Promise<MissionEvent>;
|
|
163
|
+
ack: (id: string, eventId: string, options?: RequestOptions) => Promise<MissionEvent>;
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
chats: {
|
|
167
|
+
list: (query?: Pagination, options?: RequestOptions) => Promise<Page<Chat>>;
|
|
168
|
+
get: (id: string, options?: RequestOptions & Pagination & {
|
|
169
|
+
before?: string;
|
|
170
|
+
}) => Promise<ChatDetail>;
|
|
171
|
+
/** Publish as the authenticated agent; this never creates a human command. */
|
|
172
|
+
updates: (id: string, text: string, options?: WriteOptions) => Promise<ChatEvent>;
|
|
173
|
+
};
|
|
174
|
+
chatCommands: {
|
|
175
|
+
list: (query?: Pagination, options?: RequestOptions) => Promise<Page<ChatCommand>>;
|
|
176
|
+
claim: (id: string, options?: RequestOptions) => Promise<{
|
|
177
|
+
run: ChatRun;
|
|
178
|
+
leaseToken: string;
|
|
179
|
+
}>;
|
|
180
|
+
renew: (id: string, leaseToken: string, options?: RequestOptions) => Promise<{
|
|
181
|
+
run: ChatRun;
|
|
182
|
+
}>;
|
|
183
|
+
complete: (id: string, input: {
|
|
184
|
+
leaseToken: string;
|
|
185
|
+
text: string;
|
|
186
|
+
}, options?: WriteOptions) => Promise<{
|
|
187
|
+
event: ChatEvent;
|
|
188
|
+
run: ChatRun;
|
|
189
|
+
}>;
|
|
190
|
+
fail: (id: string, input: {
|
|
191
|
+
leaseToken: string;
|
|
192
|
+
error: string;
|
|
193
|
+
}, options?: RequestOptions) => Promise<{
|
|
194
|
+
run: ChatRun;
|
|
195
|
+
}>;
|
|
196
|
+
};
|
|
197
|
+
runtime: {
|
|
198
|
+
start: (input: {
|
|
199
|
+
runId: string;
|
|
200
|
+
provider: string;
|
|
201
|
+
hostKind?: "local" | "hosted";
|
|
202
|
+
}, options?: RequestOptions) => Promise<{
|
|
203
|
+
runId: string;
|
|
204
|
+
state: string;
|
|
205
|
+
ready: boolean;
|
|
206
|
+
}>;
|
|
207
|
+
probe: (runId: string, options?: RequestOptions) => Promise<{
|
|
208
|
+
runId: string;
|
|
209
|
+
challenge: string;
|
|
210
|
+
completed: boolean;
|
|
211
|
+
protocol: string;
|
|
212
|
+
}>;
|
|
213
|
+
replyProbe: (input: {
|
|
214
|
+
runId: string;
|
|
215
|
+
reply: string;
|
|
216
|
+
}, options?: RequestOptions) => Promise<{
|
|
217
|
+
runId: string;
|
|
218
|
+
state: string;
|
|
219
|
+
ready: boolean;
|
|
220
|
+
}>;
|
|
221
|
+
heartbeat: (input?: {
|
|
222
|
+
label?: string;
|
|
223
|
+
runId?: string;
|
|
224
|
+
state?: "starting" | "online" | "offline" | "error";
|
|
225
|
+
errorCode?: string;
|
|
226
|
+
}, options?: RequestOptions) => Promise<RuntimeHeartbeat>;
|
|
227
|
+
};
|
|
228
|
+
tasks: {
|
|
229
|
+
create: (input: SendMessage, options?: WriteOptions) => Promise<Task>;
|
|
230
|
+
get: (id: string, options?: RequestOptions) => Promise<Task>;
|
|
231
|
+
list: (query?: ListTasks, options?: RequestOptions) => Promise<Page<Task>>;
|
|
232
|
+
iterate: (query?: ListTasks, options?: RequestOptions) => AsyncIterable<Task>;
|
|
233
|
+
postUpdate: (id: string, text: string, options?: WriteOptions) => Promise<Message>;
|
|
234
|
+
update: (id: string, input: {
|
|
235
|
+
state: Exclude<TaskState, "submitted" | "canceled">;
|
|
236
|
+
artifacts?: Task["artifacts"];
|
|
237
|
+
expectedVersion?: number;
|
|
238
|
+
}, options?: RequestOptions) => Promise<Task>;
|
|
239
|
+
cancel: (id: string, options?: RequestOptions) => Promise<Task>;
|
|
240
|
+
};
|
|
241
|
+
repositories: {
|
|
242
|
+
getTask: (taskId: string, options?: RequestOptions) => Promise<RepositoryCoordination>;
|
|
243
|
+
claim: (taskId: string, input: {
|
|
244
|
+
branch: string;
|
|
245
|
+
paths: string[];
|
|
246
|
+
expectedVersion?: number;
|
|
247
|
+
}, options?: RequestOptions) => Promise<RepoClaim>;
|
|
248
|
+
update: (taskId: string, input: {
|
|
249
|
+
state: Exclude<RepoClaimState, "planned">;
|
|
250
|
+
headCommit?: string;
|
|
251
|
+
pullRequestUrl?: string;
|
|
252
|
+
expectedVersion: number;
|
|
253
|
+
}, options?: RequestOptions) => Promise<RepoClaim>;
|
|
254
|
+
};
|
|
255
|
+
github: {
|
|
256
|
+
repositories: (options?: RequestOptions) => Promise<Array<{
|
|
257
|
+
repository: string;
|
|
258
|
+
repositoryId: string;
|
|
259
|
+
installationId: string;
|
|
260
|
+
}>>;
|
|
261
|
+
openPullRequest: (taskId: string, input: {
|
|
262
|
+
title: string;
|
|
263
|
+
body?: string;
|
|
264
|
+
}, options?: RequestOptions) => Promise<{
|
|
265
|
+
pullRequestUrl: string;
|
|
266
|
+
claim: RepoClaim;
|
|
267
|
+
}>;
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export declare class ConversationHandle {
|
|
271
|
+
#private;
|
|
272
|
+
readonly id: string;
|
|
273
|
+
readonly conversation: Conversation;
|
|
274
|
+
constructor(totem: Totem, conversation: Conversation, recipient: string);
|
|
275
|
+
send(text: string, options?: WriteOptions): Promise<Message>;
|
|
276
|
+
messages(query?: Pagination, options?: RequestOptions): Promise<Page<Message>>;
|
|
277
|
+
}
|
|
278
|
+
export declare class ConnectedAgent {
|
|
279
|
+
#private;
|
|
280
|
+
readonly identity: Agent;
|
|
281
|
+
readonly address: string;
|
|
282
|
+
readonly client: Totem;
|
|
283
|
+
constructor(client: Totem, identity: Agent);
|
|
284
|
+
get lastError(): Error | undefined;
|
|
285
|
+
send(input: SendMessage, options?: WriteOptions): Promise<Message>;
|
|
286
|
+
send(to: string, text: string, options?: WriteOptions): Promise<Message>;
|
|
287
|
+
startConversation(input: {
|
|
288
|
+
with: string;
|
|
289
|
+
metadata?: Metadata;
|
|
290
|
+
}): Promise<ConversationHandle>;
|
|
291
|
+
on(event: "message", handler: MessageHandler, options?: ListenOptions): Subscription;
|
|
292
|
+
stop(): Promise<void>;
|
|
293
|
+
}
|
|
294
|
+
/** Compatibility name for callers who prefer `new TotemAgent({ address, apiKey })`. */
|
|
295
|
+
export declare class TotemAgent extends Totem {
|
|
296
|
+
}
|
|
297
|
+
export { createLocalInspector, InspectionError };
|
|
298
|
+
export type { LocalInspector, LocalInspectorOptions, InspectionOptions, InspectionReceipt, InspectionKind, } from "./inspector.js";
|
package/bin/sdk/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TotemError } from "./errors.js";
|
|
2
|
+
import { createLocalInspector, InspectionError } from "./inspector.js";
|
|
2
3
|
import { idempotencyKey, sleep, Transport } from "./transport.js";
|
|
3
4
|
import { assertInputFields, assistantCheckOptions, signalOnlyOptions } from "./request-options.js";
|
|
4
5
|
export * from "./types.js";
|
|
@@ -112,6 +113,11 @@ export class Totem {
|
|
|
112
113
|
},
|
|
113
114
|
idempotencyKey: options.idempotencyKey ?? `sdk:reply:${id}`,
|
|
114
115
|
})).data,
|
|
116
|
+
replyContent: async (id, content, options = {}) => (await this.#transport.request("POST", `/messages/${encodeURIComponent(id)}/reply`, {
|
|
117
|
+
...options,
|
|
118
|
+
body: { content },
|
|
119
|
+
idempotencyKey: options.idempotencyKey ?? idempotencyKey(),
|
|
120
|
+
})).data,
|
|
115
121
|
ack: async (id, options) => (await this.#transport.request("POST", `/messages/${encodeURIComponent(id)}/ack`, {
|
|
116
122
|
...options,
|
|
117
123
|
body: {},
|
|
@@ -155,8 +161,20 @@ export class Totem {
|
|
|
155
161
|
artifacts: result.data.artifacts,
|
|
156
162
|
events: result.data.events,
|
|
157
163
|
budget: result.data.budget,
|
|
164
|
+
coordination: result.data.coordination,
|
|
158
165
|
};
|
|
159
166
|
},
|
|
167
|
+
coordination: async (id, version, options) => (await this.#transport.request("GET", `/missions/${encodeURIComponent(id)}/coordination`, {
|
|
168
|
+
...options,
|
|
169
|
+
query: { version },
|
|
170
|
+
})).data,
|
|
171
|
+
sessions: {
|
|
172
|
+
report: async (id, input, options) => (await this.#transport.request("POST", `/missions/${encodeURIComponent(id)}/sessions`, {
|
|
173
|
+
...options,
|
|
174
|
+
body: input,
|
|
175
|
+
retrySafe: true,
|
|
176
|
+
})).data,
|
|
177
|
+
},
|
|
160
178
|
workItems: async (id, options) => (await this.#transport.request("GET", `/missions/${encodeURIComponent(id)}/work-items`, options)).data.workItems,
|
|
161
179
|
/** Empty data can still have a nextCursor: readiness is checked after scanning candidates. */
|
|
162
180
|
workItemsPage: async (id, query = {}, options) => {
|
|
@@ -223,6 +241,9 @@ export class Totem {
|
|
|
223
241
|
fail: async (id, input, options) => (await this.#transport.request("POST", `/chat-commands/${encodeURIComponent(id)}/fail`, { ...options, body: input, retrySafe: true })).data,
|
|
224
242
|
};
|
|
225
243
|
runtime = {
|
|
244
|
+
start: async (input, options) => (await this.#transport.request("POST", "/runtime/start", { ...options, body: input, retrySafe: true })).data,
|
|
245
|
+
probe: async (runId, options) => (await this.#transport.request("GET", "/runtime/probe", { ...options, query: { runId } })).data,
|
|
246
|
+
replyProbe: async (input, options) => (await this.#transport.request("POST", "/runtime/probe", { ...options, body: input, retrySafe: true })).data,
|
|
226
247
|
heartbeat: async (input = {}, options) => (await this.#transport.request("POST", "/runtime/heartbeat", {
|
|
227
248
|
...options,
|
|
228
249
|
body: input,
|
|
@@ -317,6 +338,8 @@ export class ConnectedAgent {
|
|
|
317
338
|
if (!Number.isFinite(interval) || interval < 10)
|
|
318
339
|
throw new TotemError("pollIntervalMs must be at least 10.", { code: "INVALID_CONFIG" });
|
|
319
340
|
const controller = new AbortController();
|
|
341
|
+
const inspector = options.inspector ?? createLocalInspector();
|
|
342
|
+
const blocked = new Set();
|
|
320
343
|
const report = (cause) => {
|
|
321
344
|
this.#lastError = cause instanceof Error ? cause : new Error(String(cause));
|
|
322
345
|
try {
|
|
@@ -332,22 +355,42 @@ export class ConnectedAgent {
|
|
|
332
355
|
for await (const message of this.client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal: controller.signal })) {
|
|
333
356
|
if (controller.signal.aborted)
|
|
334
357
|
break;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
text: message.content.type === "text"
|
|
338
|
-
? message.content.text
|
|
339
|
-
: JSON.stringify(message.content.data),
|
|
340
|
-
reply: (text, writeOptions = {}) => this.client.messages.reply(message.id, text, {
|
|
341
|
-
...writeOptions,
|
|
342
|
-
idempotencyKey: writeOptions.idempotencyKey ?? `reply:${message.id}:0`,
|
|
343
|
-
}),
|
|
344
|
-
ack: () => this.client.messages.ack(message.id),
|
|
345
|
-
};
|
|
358
|
+
if (blocked.has(message.id))
|
|
359
|
+
continue;
|
|
346
360
|
try {
|
|
361
|
+
if (message.content.type === "encrypted")
|
|
362
|
+
throw new TotemError("Use E2eeTotem and pinned endpoint keys to process encrypted messages.", { code: "E2EE_CLIENT_REQUIRED" });
|
|
363
|
+
await inspector.assertAllowed("input", { content: message.content, metadata: message.metadata ?? {} }, { signal: controller.signal });
|
|
364
|
+
const files = message.attachments ?? [];
|
|
365
|
+
const ids = new Set(files.map((file) => file.id));
|
|
366
|
+
if (message.content.type === "text" &&
|
|
367
|
+
message.content.attachments?.some((id) => !ids.has(id)))
|
|
368
|
+
throw new InspectionError("INSPECTION_UNSUPPORTED_FILE");
|
|
369
|
+
for (const file of files) {
|
|
370
|
+
const downloaded = await this.client.attachments.download(file.id, {
|
|
371
|
+
signal: controller.signal,
|
|
372
|
+
});
|
|
373
|
+
await inspector.inspectFile(file.name, file.mediaType, downloaded.bytes, {
|
|
374
|
+
signal: controller.signal,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const incoming = {
|
|
378
|
+
...message,
|
|
379
|
+
text: message.content.type === "text"
|
|
380
|
+
? message.content.text
|
|
381
|
+
: JSON.stringify(message.content.data),
|
|
382
|
+
reply: (text, writeOptions = {}) => inspector.runAction({ id: message.id, text }, (input) => this.client.messages.reply(input.id, input.text, {
|
|
383
|
+
...writeOptions,
|
|
384
|
+
idempotencyKey: writeOptions.idempotencyKey ?? `reply:${message.id}:0`,
|
|
385
|
+
}), { signal: controller.signal }),
|
|
386
|
+
ack: () => this.client.messages.ack(message.id),
|
|
387
|
+
};
|
|
347
388
|
await handler(incoming);
|
|
348
389
|
await incoming.ack();
|
|
349
390
|
}
|
|
350
391
|
catch (error) {
|
|
392
|
+
if (error instanceof InspectionError && error.code !== "INSPECTION_UNAVAILABLE")
|
|
393
|
+
blocked.add(message.id);
|
|
351
394
|
report(error);
|
|
352
395
|
}
|
|
353
396
|
}
|
|
@@ -385,3 +428,4 @@ export class ConnectedAgent {
|
|
|
385
428
|
/** Compatibility name for callers who prefer `new TotemAgent({ address, apiKey })`. */
|
|
386
429
|
export class TotemAgent extends Totem {
|
|
387
430
|
}
|
|
431
|
+
export { createLocalInspector, InspectionError };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Customer-side screening. A passing inspection is never an authorization grant. */
|
|
2
|
+
export type InspectionKind = "input" | "tool-result" | "output" | "action" | "file";
|
|
3
|
+
export type InspectionCode = "INSPECTION_BLOCKED" | "INSPECTION_UNAVAILABLE" | "INSPECTION_LIMIT" | "INSPECTION_UNSUPPORTED_FILE";
|
|
4
|
+
export declare class InspectionError extends Error {
|
|
5
|
+
readonly code: InspectionCode;
|
|
6
|
+
readonly status = 422;
|
|
7
|
+
constructor(code: InspectionCode);
|
|
8
|
+
}
|
|
9
|
+
export interface InspectionOptions {
|
|
10
|
+
/** Trusted original user intent; never replace this with a peer's instructions. */
|
|
11
|
+
task?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface InspectionReceipt {
|
|
15
|
+
version: 1;
|
|
16
|
+
policy: "totem-local-v1";
|
|
17
|
+
kind: InspectionKind;
|
|
18
|
+
mode: "rules" | "rules+classifier";
|
|
19
|
+
/** SHA-256 of the entire kind, task, and value actually screened. No plaintext. */
|
|
20
|
+
digest: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LocalInspectorOptions {
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
classifier?: {
|
|
25
|
+
/** Exact loopback OpenAI-compatible chat endpoint. Redirects are prohibited. */
|
|
26
|
+
endpoint: string;
|
|
27
|
+
model: string;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Customer-controlled transport, primarily for isolated tests. */
|
|
31
|
+
fetch?: typeof globalThis.fetch;
|
|
32
|
+
}
|
|
33
|
+
export interface LocalInspector {
|
|
34
|
+
readonly mode: InspectionReceipt["mode"];
|
|
35
|
+
assertAllowed(kind: InspectionKind, value: unknown, options?: InspectionOptions): Promise<InspectionReceipt>;
|
|
36
|
+
inspectFile(name: string, mediaType: string, bytes: Uint8Array, options?: InspectionOptions): Promise<InspectionReceipt>;
|
|
37
|
+
/** Execute only the immutable argument snapshot that passed inspection. */
|
|
38
|
+
runAction<T, R>(value: T, operation: (inspected: T) => Promise<R>, options?: InspectionOptions): Promise<R>;
|
|
39
|
+
}
|
|
40
|
+
export declare function createLocalInspector(options?: LocalInspectorOptions): LocalInspector;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestOptions } from "./types.js";
|
|
2
|
+
export declare function requireActionChecksKey(apiKey: string): string;
|
|
3
|
+
export declare function assistantCheckOptions(options: RequestOptions & {
|
|
4
|
+
actionChecksApiKey: string;
|
|
5
|
+
}): {
|
|
6
|
+
headers: {
|
|
7
|
+
"X-Totem-Action-Key": string;
|
|
8
|
+
};
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
export declare function assertInputFields(input: object, allowed: string[]): void;
|
|
12
|
+
/** These APIs derive caller identity from credentials, never from caller options. */
|
|
13
|
+
export declare function signalOnlyOptions(options?: RequestOptions): RequestOptions;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RequestOptions, TotemOptions } from "./types.js";
|
|
2
|
+
export declare function idempotencyKey(): string;
|
|
3
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
4
|
+
export declare class Transport {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(options: TotemOptions, api?: {
|
|
7
|
+
basePath: "/api/action-checks";
|
|
8
|
+
responseEnvelope: "object";
|
|
9
|
+
} | undefined);
|
|
10
|
+
request<T>(method: "GET" | "POST" | "PATCH", path: string, options?: RequestOptions & {
|
|
11
|
+
body?: unknown;
|
|
12
|
+
query?: object;
|
|
13
|
+
idempotencyKey?: string;
|
|
14
|
+
retrySafe?: boolean;
|
|
15
|
+
binaryBody?: Uint8Array;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}): Promise<T>;
|
|
18
|
+
download(path: string, options?: RequestOptions): Promise<{
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
mediaType: string;
|
|
21
|
+
name: string;
|
|
22
|
+
sha256: string | null;
|
|
23
|
+
}>;
|
|
24
|
+
}
|