@finalai/final-agent-provider 0.5.0 → 0.6.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/HOSTING.md +3 -3
- package/README.md +4 -0
- package/WORKSPACE-EGRESS.md +7 -6
- package/dist/modules/channels/projection.d.mts +13 -7
- package/dist/modules/channels/projection.mjs +49 -31
- package/dist/modules/channels/projection.mjs.map +2 -2
- package/dist/modules/sandboxes/computer.d.mts +4 -3
- package/dist/modules/sandboxes/computer.mjs +22 -35
- package/dist/modules/sandboxes/computer.mjs.map +2 -2
- package/dist/modules/sandboxes/computers-api.d.mts +2 -17
- package/dist/modules/sandboxes/computers-api.mjs +27 -44
- package/dist/modules/sandboxes/computers-api.mjs.map +2 -2
- package/dist/modules/sandboxes/device-api.d.mts +3 -8
- package/dist/modules/sandboxes/device-api.mjs +17 -28
- package/dist/modules/sandboxes/device-api.mjs.map +3 -3
- package/dist/modules/sandboxes/device.d.mts +2 -1
- package/dist/modules/sandboxes/device.mjs +13 -5
- package/dist/modules/sandboxes/device.mjs.map +2 -2
- package/dist/modules/sandboxes/egress.d.mts +4 -1
- package/dist/modules/sandboxes/egress.mjs +20 -2
- package/dist/modules/sandboxes/egress.mjs.map +2 -2
- package/dist/modules/sandboxes/environment.d.mts +0 -3
- package/dist/modules/sandboxes/environment.mjs +2 -8
- package/dist/modules/sandboxes/environment.mjs.map +2 -2
- package/dist/modules/sandboxes/index.d.mts +1 -1
- package/dist/modules/sandboxes/index.mjs +0 -2
- package/dist/modules/sandboxes/index.mjs.map +2 -2
- package/dist/modules/sandboxes/testing/fake-backends.mjs +2 -2
- package/dist/modules/sandboxes/testing/fake-backends.mjs.map +2 -2
- package/package.json +5 -5
package/HOSTING.md
CHANGED
|
@@ -7,13 +7,13 @@ resolve against dev and production configuration without source edits.
|
|
|
7
7
|
## Selecting a deployment
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
export FINAL_API_HOST=https
|
|
11
|
-
export FINAL_API_KEY=<dev-
|
|
10
|
+
export FINAL_API_HOST=https://<dev-agents-api-host>/api
|
|
11
|
+
export FINAL_API_KEY=<dev-api-key>
|
|
12
12
|
pnpm exec final-agents build ./agent
|
|
13
13
|
pnpm exec final-agents publish ./agent --agent my-agent
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Choose production by changing the host and
|
|
16
|
+
Choose production by changing the host and API key. `--host` overrides
|
|
17
17
|
`FINAL_API_HOST`; without either, the existing production default remains. Build
|
|
18
18
|
compiles locally and optionally checks installed-harness compatibility. Publish
|
|
19
19
|
compiles again and sends source/release requests to the selected deployment.
|
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ Optional Node ACP adapters are exposed under `/runtime/*`. Sandbox adapters use
|
|
|
8
8
|
the public Computers and Device APIs; the Computers transport is shared with
|
|
9
9
|
`@finalai/computers`. Runtime configuration and credentials are injected.
|
|
10
10
|
|
|
11
|
+
The `computer` and `device` sandboxes authenticate with the context's
|
|
12
|
+
`credentials.computersAccess()` and `credentials.deviceAccess()` (`{ baseUrl, token }`);
|
|
13
|
+
without them they fail with `credential_missing`.
|
|
14
|
+
|
|
11
15
|
Authoring helpers produce manifest bindings. Select the matching `/runtime/*`
|
|
12
16
|
adapter when connecting your own CLI or ACP endpoint.
|
|
13
17
|
|
package/WORKSPACE-EGRESS.md
CHANGED
|
@@ -12,12 +12,12 @@ a public replacement-Device protocol. Compose it with a provider through the sam
|
|
|
12
12
|
```ts
|
|
13
13
|
const networking = createWorkspaceDeviceNetworking({ api: deviceApi, needed });
|
|
14
14
|
const computers = createComputerSandboxProvider({ computersApi, networking });
|
|
15
|
-
const daytona = createDaytonaSandboxProvider({
|
|
15
|
+
const daytona = createDaytonaSandboxProvider({ networking });
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The factories are exported from `/sandboxes/egress`, `/sandboxes/computer`, and
|
|
19
19
|
`@finalai/daytona-agent-provider/sandboxes/daytona`, respectively. Keep the trusted
|
|
20
|
-
Device
|
|
20
|
+
Device access in the context's credentials. A custom provider accepts
|
|
21
21
|
the same generic extension: call `enabled`, validate its workspace identity, call
|
|
22
22
|
`prepare` before provider fencing, and use `attachSandboxNetworking` after
|
|
23
23
|
obtaining its raw handle. Supply the saved policy, restore/fence callbacks,
|
|
@@ -25,19 +25,20 @@ namespace existence check and deferred setup. Physical teardown calls `suspend`
|
|
|
25
25
|
even if another cleanup step fails. Providers needing earlier namespace setup can
|
|
26
26
|
use `controller` and `withSandboxNetworking` directly, as Final Computer does.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
The context's `credentials.deviceAccess({ actingUserId })` returns the Device API
|
|
29
|
+
origin and a token. Device
|
|
29
30
|
operations use the current actor; background lease reconciliation may use the stored
|
|
30
31
|
lease owner instead of a present turn author. Hosts must authorize both forms of work.
|
|
31
32
|
Admission, policy persistence, and cleanup also make trusted service calls with
|
|
32
|
-
`actingUserId: null`. Enabling workspace Device networking requires
|
|
33
|
+
`actingUserId: null`. Enabling workspace Device networking requires access
|
|
33
34
|
accepted by those service endpoints for that case, separate from user-bound grants.
|
|
34
35
|
Returning `null` refuses the call before HTTP; a user-token-only configuration cannot
|
|
35
36
|
start or clean up networked workspaces. Its host must add service authentication
|
|
36
37
|
before enabling this extension. The SDK never reuses a user's cached grant for a
|
|
37
|
-
service call
|
|
38
|
+
service call or another user: it asks the context's credentials on every call.
|
|
38
39
|
|
|
39
40
|
The extension owns Device authorization, enrollment, helper installation, leases
|
|
40
|
-
and failover. Providers need no Device
|
|
41
|
+
and failover. Providers need no Device networking implementation. Its generic
|
|
41
42
|
controller also authorizes external editor access and reports its expiry limit;
|
|
42
43
|
providers must bound editor grants by that deadline. The Device executable and
|
|
43
44
|
hosted control services remain separate first-party implementations.
|
|
@@ -3,13 +3,19 @@ import { TurnPolicy, type JsonValue, type ProjectionContextIface } from '@finala
|
|
|
3
3
|
import * as z from 'zod';
|
|
4
4
|
export declare const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;
|
|
5
5
|
export declare const projectionStateSchema: z.ZodObject<{
|
|
6
|
-
messageId: z.ZodNullable<z.ZodString
|
|
6
|
+
messageId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
7
7
|
messageSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
8
|
-
fromSeq: z.ZodNullable<z.ZodNumber
|
|
9
|
-
lastSeq: z.ZodNullable<z.ZodNumber
|
|
10
|
-
lastFlushAt: z.ZodNumber
|
|
11
|
-
dirty: z.ZodBoolean
|
|
8
|
+
fromSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
9
|
+
lastSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
10
|
+
lastFlushAt: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
dirty: z.ZodDefault<z.ZodBoolean>;
|
|
12
12
|
segments: z.ZodArray<z.ZodObject<{
|
|
13
|
+
messageId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
14
|
+
messageSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
15
|
+
fromSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
16
|
+
lastSeq: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
17
|
+
lastFlushAt: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
dirty: z.ZodDefault<z.ZodBoolean>;
|
|
13
19
|
acpMessageId: z.ZodString;
|
|
14
20
|
blocks: z.ZodArray<z.ZodType<import("@finalai/agent-provider").JsonObject, unknown, z.core.$ZodTypeInternals<import("@finalai/agent-provider").JsonObject, unknown>>>;
|
|
15
21
|
}, z.core.$strip>>;
|
|
@@ -22,8 +28,8 @@ export interface ProjectorConfigIface {
|
|
|
22
28
|
}
|
|
23
29
|
export declare const emptyProjectionState: () => ProjectionStateIface;
|
|
24
30
|
export declare const projectionStateOf: (state: JsonValue | null) => ProjectionStateIface;
|
|
25
|
-
export declare const visibleContent: (state: ProjectionStateIface) => ContentBlock[];
|
|
26
|
-
export declare const accumulateUpdate: (state: ProjectionStateIface, update: SessionUpdate) =>
|
|
31
|
+
export declare const visibleContent: (state: Pick<ProjectionStateIface, "segments">) => ContentBlock[];
|
|
32
|
+
export declare const accumulateUpdate: (state: ProjectionStateIface, update: SessionUpdate) => ProjectionStateIface["segments"][number] | null;
|
|
27
33
|
export interface ProjectorIface {
|
|
28
34
|
project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;
|
|
29
35
|
}
|
|
@@ -8,16 +8,21 @@ import {
|
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { contentBlockOf, jsonOfContentBlock } from "./content.mjs";
|
|
10
10
|
const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
messageId: z.string().nullable(),
|
|
11
|
+
const projectedMessageSchema = z.object({
|
|
12
|
+
messageId: z.string().nullable().default(null),
|
|
14
13
|
// The reply's conversation seq, so the finished reply is read back by
|
|
15
14
|
// position however many messages arrived meanwhile.
|
|
16
15
|
messageSeq: z.number().nullable().default(null),
|
|
17
|
-
fromSeq: z.number().nullable(),
|
|
18
|
-
lastSeq: z.number().nullable(),
|
|
19
|
-
lastFlushAt: z.number(),
|
|
20
|
-
dirty: z.boolean()
|
|
16
|
+
fromSeq: z.number().nullable().default(null),
|
|
17
|
+
lastSeq: z.number().nullable().default(null),
|
|
18
|
+
lastFlushAt: z.number().default(0),
|
|
19
|
+
dirty: z.boolean().default(false)
|
|
20
|
+
});
|
|
21
|
+
const segmentSchema = projectedMessageSchema.extend({
|
|
22
|
+
acpMessageId: z.string(),
|
|
23
|
+
blocks: z.array(jsonObjectSchema)
|
|
24
|
+
});
|
|
25
|
+
const projectionStateSchema = projectedMessageSchema.extend({
|
|
21
26
|
segments: z.array(segmentSchema)
|
|
22
27
|
});
|
|
23
28
|
const agentChunkSchema = z.object({
|
|
@@ -79,7 +84,7 @@ const segmentFor = (state, acpMessageId) => {
|
|
|
79
84
|
if (existing != null) {
|
|
80
85
|
return existing;
|
|
81
86
|
}
|
|
82
|
-
const segment = { acpMessageId, blocks: [] };
|
|
87
|
+
const segment = segmentSchema.parse({ acpMessageId, blocks: [] });
|
|
83
88
|
state.segments.push(segment);
|
|
84
89
|
return segment;
|
|
85
90
|
};
|
|
@@ -88,35 +93,39 @@ const accumulateUpdate = (state, update) => {
|
|
|
88
93
|
const chunk = agentChunkSchema.safeParse(json);
|
|
89
94
|
if (chunk.success) {
|
|
90
95
|
const block = contentBlockOf(chunk.data.content);
|
|
91
|
-
if (block == null) return
|
|
92
|
-
segmentFor(state, chunk.data.messageId)
|
|
93
|
-
|
|
96
|
+
if (block == null) return null;
|
|
97
|
+
const segment = segmentFor(state, chunk.data.messageId);
|
|
98
|
+
segment.blocks.push(jsonOfContentBlock(block));
|
|
99
|
+
return segment;
|
|
94
100
|
}
|
|
95
101
|
const message = agentMessageSchema.safeParse(json);
|
|
96
102
|
if (message.success) {
|
|
97
|
-
if (message.data.content === void 0) return
|
|
103
|
+
if (message.data.content === void 0) return null;
|
|
98
104
|
const blocks = (message.data.content ?? []).flatMap((block) => {
|
|
99
105
|
const parsed = contentBlockOf(block);
|
|
100
106
|
return parsed == null ? [] : [jsonOfContentBlock(parsed)];
|
|
101
107
|
});
|
|
102
|
-
segmentFor(state, message.data.messageId)
|
|
103
|
-
|
|
108
|
+
const segment = segmentFor(state, message.data.messageId);
|
|
109
|
+
segment.blocks = blocks;
|
|
110
|
+
return segment;
|
|
104
111
|
}
|
|
105
|
-
return
|
|
112
|
+
return null;
|
|
106
113
|
};
|
|
107
114
|
const createProjector = (config = {}) => {
|
|
108
115
|
const now = config.now ?? Date.now;
|
|
109
116
|
const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;
|
|
110
117
|
const fold = async (update, ctx) => {
|
|
111
118
|
const state = projectionStateOf(ctx.state.get());
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
119
|
+
const segment = accumulateUpdate(state, update);
|
|
120
|
+
if (segment == null) return;
|
|
121
|
+
const target = state.messageId == null ? segment : state;
|
|
122
|
+
target.lastSeq = ctx.seq;
|
|
123
|
+
const content = visibleContent({ segments: target === state ? state.segments : [segment] });
|
|
124
|
+
if (content.length === 0 && target.messageId == null) {
|
|
116
125
|
ctx.state.set(state);
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
119
|
-
if (
|
|
128
|
+
if (target.messageId == null) {
|
|
120
129
|
const message = await ctx.platform.conversations.postMessage({
|
|
121
130
|
conversationId: ctx.conversation.conversationId,
|
|
122
131
|
authorParticipantId: ctx.agentParticipant.id,
|
|
@@ -124,32 +133,41 @@ const createProjector = (config = {}) => {
|
|
|
124
133
|
content,
|
|
125
134
|
source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq }
|
|
126
135
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} else if (now() -
|
|
136
|
+
target.messageId = message.messageId;
|
|
137
|
+
target.messageSeq = message.seq;
|
|
138
|
+
target.fromSeq = ctx.seq;
|
|
139
|
+
target.lastFlushAt = now();
|
|
140
|
+
target.dirty = false;
|
|
141
|
+
} else if (now() - target.lastFlushAt >= interval) {
|
|
133
142
|
await ctx.platform.conversations.updateMessage({
|
|
134
|
-
messageId:
|
|
143
|
+
messageId: target.messageId,
|
|
135
144
|
content,
|
|
136
145
|
final: false
|
|
137
146
|
});
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
target.lastFlushAt = now();
|
|
148
|
+
target.dirty = false;
|
|
140
149
|
} else {
|
|
141
|
-
|
|
150
|
+
target.dirty = true;
|
|
142
151
|
}
|
|
143
152
|
ctx.state.set(state);
|
|
144
153
|
};
|
|
145
154
|
const finish = async (ctx) => {
|
|
146
155
|
const state = projectionStateOf(ctx.state.get());
|
|
156
|
+
if (state.messageId != null) {
|
|
157
|
+
await finishMessage(ctx, state, visibleContent(state));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const segment of state.segments) {
|
|
161
|
+
await finishMessage(ctx, segment, visibleContent({ segments: [segment] }));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const finishMessage = async (ctx, state, content) => {
|
|
147
165
|
if (state.messageId == null) {
|
|
148
166
|
return;
|
|
149
167
|
}
|
|
150
168
|
await ctx.platform.conversations.updateMessage({
|
|
151
169
|
messageId: state.messageId,
|
|
152
|
-
content
|
|
170
|
+
content,
|
|
153
171
|
final: true
|
|
154
172
|
});
|
|
155
173
|
const reply = state.messageSeq == null ? null : (await ctx.platform.conversations.recentMessages({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/channels/projection.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ContentBlock, SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';\nimport { textOfBlock } from '@finalai/acp-session';\nimport {\n jsonObjectSchema,\n MessageKind,\n turnEndOf,\n TurnPolicy,\n type JsonValue,\n type ProjectionContextIface,\n} from '@finalai/agent-provider';\nimport * as z from 'zod';\nimport { contentBlockOf, jsonOfContentBlock } from \"./content.mjs\";\n\n// agent_message_chunk accumulates into one visible message per turn\n// segment. The message is created on the first visible chunk (streaming),\n// updated at most every `updateIntervalMs`, and finalized at the idle\n// state_update that ends the turn. The fold lives in the turn state the\n// platform keeps (ctx.state). An empty turn projects nothing.\n\nexport const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;\n\nconst segmentSchema = z.object({ acpMessageId: z.string(), blocks: z.array(jsonObjectSchema) });\n\nexport const projectionStateSchema = z.object({\n messageId: z.string().nullable(),\n // The reply's conversation seq, so the finished reply is read back by\n // position however many messages arrived meanwhile.\n messageSeq: z.number().nullable().default(null),\n fromSeq: z.number().nullable(),\n lastSeq: z.number().nullable(),\n lastFlushAt: z.number(),\n dirty: z.boolean(),\n segments: z.array(segmentSchema),\n});\n\nexport type ProjectionStateIface = z.infer<typeof projectionStateSchema>;\n\nexport interface ProjectorConfigIface {\n updateIntervalMs?: number;\n now?: () => number;\n // The channel's turnPolicy, for routing finished replies (default steer).\n turnPolicy?: TurnPolicy;\n}\n\nconst agentChunkSchema = z.object({\n sessionUpdate: z.literal('agent_message_chunk'),\n messageId: z.string(),\n content: jsonObjectSchema,\n});\n\nconst agentMessageSchema = z.object({\n sessionUpdate: z.literal('agent_message'),\n messageId: z.string(),\n content: z.array(jsonObjectSchema).nullish(),\n});\n\nconst updateJson = (update: SessionUpdate): JsonValue => {\n const parsed = jsonObjectSchema.safeParse(JSON.parse(JSON.stringify(update)));\n return parsed.success ? parsed.data : null;\n};\n\nexport const emptyProjectionState = (): ProjectionStateIface => ({\n messageId: null,\n messageSeq: null,\n fromSeq: null,\n lastSeq: null,\n lastFlushAt: 0,\n dirty: false,\n segments: [],\n});\n\nexport const projectionStateOf = (state: JsonValue | null): ProjectionStateIface => {\n const parsed = projectionStateSchema.safeParse(state);\n return parsed.success ? parsed.data : emptyProjectionState();\n};\n\n// Plain text (no `_meta`) of a block, the only kind consecutive chunks merge into.\nconst plainText = (block: ContentBlock | undefined): string | null =>\n block == null || block._meta != null ? null : textOfBlock(block);\n\nconst appendBlock = (blocks: ContentBlock[], block: ContentBlock, separator: string): void => {\n const lastText = plainText(blocks.at(-1));\n const nextText = plainText(block);\n if (lastText != null && nextText != null) {\n blocks[blocks.length - 1] = { type: 'text', text: `${lastText}${separator}${nextText}` };\n } else {\n blocks.push(block);\n }\n};\n\n// The visible content of the turn: segments in order of first appearance,\n// separated by a blank line between text runs.\nexport const visibleContent = (state: ProjectionStateIface): ContentBlock[] => {\n const content: ContentBlock[] = [];\n for (const segment of state.segments) {\n const blocks: ContentBlock[] = [];\n for (const block of segment.blocks) {\n const parsed = contentBlockOf(block);\n if (parsed != null) appendBlock(blocks, parsed, '');\n }\n for (const block of blocks) appendBlock(content, block, '\\n\\n');\n }\n return content.flatMap((block): ContentBlock[] => {\n const text = textOfBlock(block);\n if (text == null) return [block];\n const trimmed = text.trim();\n return trimmed.length === 0 ? [] : [{ ...block, type: 'text', text: trimmed }];\n });\n};\n\nconst segmentFor = (\n state: ProjectionStateIface,\n acpMessageId: string,\n): ProjectionStateIface['segments'][number] => {\n const existing = state.segments.find((segment) => segment.acpMessageId === acpMessageId);\n if (existing != null) {\n return existing;\n }\n const segment = { acpMessageId, blocks: [] };\n state.segments.push(segment);\n return segment;\n};\n\n// Folds one ACP update into the state. Returns false for updates that are not\n// visible output.\nexport const accumulateUpdate = (state: ProjectionStateIface, update: SessionUpdate): boolean => {\n const json = updateJson(update);\n const chunk = agentChunkSchema.safeParse(json);\n if (chunk.success) {\n const block = contentBlockOf(chunk.data.content);\n if (block == null) return false;\n segmentFor(state, chunk.data.messageId).blocks.push(jsonOfContentBlock(block));\n return true;\n }\n const message = agentMessageSchema.safeParse(json);\n if (message.success) {\n // ACP v2 upsert: a concrete array replaces the message's content; omitted leaves it.\n if (message.data.content === undefined) return false;\n const blocks = (message.data.content ?? []).flatMap((block) => {\n const parsed = contentBlockOf(block);\n return parsed == null ? [] : [jsonOfContentBlock(parsed)];\n });\n segmentFor(state, message.data.messageId).blocks = blocks;\n return true;\n }\n return false;\n};\n\ninterface ConversationProjectionContextIface extends ProjectionContextIface {\n conversation: NonNullable<ProjectionContextIface['conversation']>;\n agentParticipant: NonNullable<ProjectionContextIface['agentParticipant']>;\n}\n\nexport interface ProjectorIface {\n // ChannelDefinitionIface['project'] for the messaging channels: folds visible\n // output into the turn's message and, at the turn-ending idle state_update,\n // finalizes and routes it.\n project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;\n}\n\nexport const createProjector = (config: ProjectorConfigIface = {}): ProjectorIface => {\n const now = config.now ?? Date.now;\n const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;\n\n const fold = async (\n update: SessionUpdate,\n ctx: ConversationProjectionContextIface,\n ): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (!accumulateUpdate(state, update)) return;\n state.lastSeq = ctx.seq;\n const content = visibleContent(state);\n if (content.length === 0) {\n ctx.state.set(state);\n return;\n }\n if (state.messageId == null) {\n const message = await ctx.platform.conversations.postMessage({\n conversationId: ctx.conversation.conversationId,\n authorParticipantId: ctx.agentParticipant.id,\n kind: MessageKind.Message,\n content,\n source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq },\n });\n state.messageId = message.messageId;\n state.messageSeq = message.seq;\n state.fromSeq = ctx.seq;\n state.lastFlushAt = now();\n state.dirty = false;\n } else if (now() - state.lastFlushAt >= interval) {\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content,\n final: false,\n });\n state.lastFlushAt = now();\n state.dirty = false;\n } else {\n state.dirty = true;\n }\n ctx.state.set(state);\n };\n\n const finish = async (ctx: ConversationProjectionContextIface): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (state.messageId == null) {\n return;\n }\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content: visibleContent(state),\n final: true,\n });\n // The finished reply is routed like any message: other agents may observe\n // it or, when mentioned within the hop limit, be prompted.\n const reply =\n state.messageSeq == null\n ? null\n : (\n await ctx.platform.conversations.recentMessages({\n conversationId: ctx.conversation.conversationId,\n beforeSeq: state.messageSeq + 1,\n limit: 1,\n })\n ).find((message) => message.messageId === state.messageId);\n if (reply == null) {\n ctx.platform.log('finished reply not found for routing', {\n messageId: state.messageId,\n seq: state.messageSeq,\n });\n }\n if (reply != null) {\n if (ctx.platform.conversations.dispatchMessage == null)\n throw new Error('channel dispatch is unavailable');\n await ctx.platform.conversations.dispatchMessage({\n conversationId: reply.conversationId,\n messageId: reply.messageId,\n });\n }\n };\n\n const project: ProjectorIface['project'] = async (update, ctx) => {\n const { conversation, agentParticipant } = ctx;\n if (conversation == null || agentParticipant == null)\n throw new Error('Final rendering requires a Final conversation');\n const context = { ...ctx, conversation, agentParticipant };\n return turnEndOf(update) == null ? fold(update, context) : finish(context);\n };\n\n return { project };\n};\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,YAAY,OAAO;AACnB,SAAS,gBAAgB,0BAA0B;AAQ5C,MAAM,wCAAwC;AAErD,MAAM,
|
|
4
|
+
"sourcesContent": ["import type { ContentBlock, SessionUpdate } from '@agentclientprotocol/sdk/experimental/v2';\nimport { textOfBlock } from '@finalai/acp-session';\nimport {\n jsonObjectSchema,\n MessageKind,\n turnEndOf,\n TurnPolicy,\n type JsonValue,\n type ProjectionContextIface,\n} from '@finalai/agent-provider';\nimport * as z from 'zod';\nimport { contentBlockOf, jsonOfContentBlock } from \"./content.mjs\";\n\n// Each ACP message ID owns a visible conversation message. The message is\n// created on its first visible chunk (streaming),\n// updated at most every `updateIntervalMs`, and finalized at the idle\n// state_update that ends the turn. The fold lives in the turn state the\n// platform keeps (ctx.state). An empty turn projects nothing.\n\nexport const DEFAULT_PROJECTION_UPDATE_INTERVAL_MS = 300;\n\nconst projectedMessageSchema = z.object({\n messageId: z.string().nullable().default(null),\n // The reply's conversation seq, so the finished reply is read back by\n // position however many messages arrived meanwhile.\n messageSeq: z.number().nullable().default(null),\n fromSeq: z.number().nullable().default(null),\n lastSeq: z.number().nullable().default(null),\n lastFlushAt: z.number().default(0),\n dirty: z.boolean().default(false),\n});\n\nconst segmentSchema = projectedMessageSchema.extend({\n acpMessageId: z.string(),\n blocks: z.array(jsonObjectSchema),\n});\n\n// Root message fields are retained for turns created by the old projector.\n// Those turns finish in their existing combined row; new turns use segment rows.\nexport const projectionStateSchema = projectedMessageSchema.extend({\n segments: z.array(segmentSchema),\n});\n\nexport type ProjectionStateIface = z.infer<typeof projectionStateSchema>;\n\nexport interface ProjectorConfigIface {\n updateIntervalMs?: number;\n now?: () => number;\n // The channel's turnPolicy, for routing finished replies (default steer).\n turnPolicy?: TurnPolicy;\n}\n\nconst agentChunkSchema = z.object({\n sessionUpdate: z.literal('agent_message_chunk'),\n messageId: z.string(),\n content: jsonObjectSchema,\n});\n\nconst agentMessageSchema = z.object({\n sessionUpdate: z.literal('agent_message'),\n messageId: z.string(),\n content: z.array(jsonObjectSchema).nullish(),\n});\n\nconst updateJson = (update: SessionUpdate): JsonValue => {\n const parsed = jsonObjectSchema.safeParse(JSON.parse(JSON.stringify(update)));\n return parsed.success ? parsed.data : null;\n};\n\nexport const emptyProjectionState = (): ProjectionStateIface => ({\n messageId: null,\n messageSeq: null,\n fromSeq: null,\n lastSeq: null,\n lastFlushAt: 0,\n dirty: false,\n segments: [],\n});\n\nexport const projectionStateOf = (state: JsonValue | null): ProjectionStateIface => {\n const parsed = projectionStateSchema.safeParse(state);\n return parsed.success ? parsed.data : emptyProjectionState();\n};\n\n// Plain text (no `_meta`) of a block, the only kind consecutive chunks merge into.\nconst plainText = (block: ContentBlock | undefined): string | null =>\n block == null || block._meta != null ? null : textOfBlock(block);\n\nconst appendBlock = (blocks: ContentBlock[], block: ContentBlock, separator: string): void => {\n const lastText = plainText(blocks.at(-1));\n const nextText = plainText(block);\n if (lastText != null && nextText != null) {\n blocks[blocks.length - 1] = { type: 'text', text: `${lastText}${separator}${nextText}` };\n } else {\n blocks.push(block);\n }\n};\n\n// The visible content of the turn: segments in order of first appearance,\n// separated by a blank line between text runs.\nexport const visibleContent = (state: Pick<ProjectionStateIface, 'segments'>): ContentBlock[] => {\n const content: ContentBlock[] = [];\n for (const segment of state.segments) {\n const blocks: ContentBlock[] = [];\n for (const block of segment.blocks) {\n const parsed = contentBlockOf(block);\n if (parsed != null) appendBlock(blocks, parsed, '');\n }\n for (const block of blocks) appendBlock(content, block, '\\n\\n');\n }\n return content.flatMap((block): ContentBlock[] => {\n const text = textOfBlock(block);\n if (text == null) return [block];\n const trimmed = text.trim();\n return trimmed.length === 0 ? [] : [{ ...block, type: 'text', text: trimmed }];\n });\n};\n\nconst segmentFor = (\n state: ProjectionStateIface,\n acpMessageId: string,\n): ProjectionStateIface['segments'][number] => {\n const existing = state.segments.find((segment) => segment.acpMessageId === acpMessageId);\n if (existing != null) {\n return existing;\n }\n const segment = segmentSchema.parse({ acpMessageId, blocks: [] });\n state.segments.push(segment);\n return segment;\n};\n\n// Folds one ACP update into the state and returns the affected segment, or null\n// for updates that are not visible output.\nexport const accumulateUpdate = (\n state: ProjectionStateIface,\n update: SessionUpdate,\n): ProjectionStateIface['segments'][number] | null => {\n const json = updateJson(update);\n const chunk = agentChunkSchema.safeParse(json);\n if (chunk.success) {\n const block = contentBlockOf(chunk.data.content);\n if (block == null) return null;\n const segment = segmentFor(state, chunk.data.messageId);\n segment.blocks.push(jsonOfContentBlock(block));\n return segment;\n }\n const message = agentMessageSchema.safeParse(json);\n if (message.success) {\n // ACP v2 upsert: a concrete array replaces the message's content; omitted leaves it.\n if (message.data.content === undefined) return null;\n const blocks = (message.data.content ?? []).flatMap((block) => {\n const parsed = contentBlockOf(block);\n return parsed == null ? [] : [jsonOfContentBlock(parsed)];\n });\n const segment = segmentFor(state, message.data.messageId);\n segment.blocks = blocks;\n return segment;\n }\n return null;\n};\n\ninterface ConversationProjectionContextIface extends ProjectionContextIface {\n conversation: NonNullable<ProjectionContextIface['conversation']>;\n agentParticipant: NonNullable<ProjectionContextIface['agentParticipant']>;\n}\n\nexport interface ProjectorIface {\n // ChannelDefinitionIface['project'] for the messaging channels: folds visible\n // output into separate messages and, at the turn-ending idle state_update,\n // finalizes and routes them in order of first appearance.\n project(update: SessionUpdate, ctx: ProjectionContextIface): Promise<void>;\n}\n\nexport const createProjector = (config: ProjectorConfigIface = {}): ProjectorIface => {\n const now = config.now ?? Date.now;\n const interval = config.updateIntervalMs ?? DEFAULT_PROJECTION_UPDATE_INTERVAL_MS;\n\n const fold = async (\n update: SessionUpdate,\n ctx: ConversationProjectionContextIface,\n ): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n const segment = accumulateUpdate(state, update);\n if (segment == null) return;\n const target = state.messageId == null ? segment : state;\n target.lastSeq = ctx.seq;\n const content = visibleContent({ segments: target === state ? state.segments : [segment] });\n if (content.length === 0 && target.messageId == null) {\n ctx.state.set(state);\n return;\n }\n if (target.messageId == null) {\n const message = await ctx.platform.conversations.postMessage({\n conversationId: ctx.conversation.conversationId,\n authorParticipantId: ctx.agentParticipant.id,\n kind: MessageKind.Message,\n content,\n source: { turnId: ctx.turn.id, fromSeq: ctx.seq, toSeq: ctx.seq },\n });\n target.messageId = message.messageId;\n target.messageSeq = message.seq;\n target.fromSeq = ctx.seq;\n target.lastFlushAt = now();\n target.dirty = false;\n } else if (now() - target.lastFlushAt >= interval) {\n await ctx.platform.conversations.updateMessage({\n messageId: target.messageId,\n content,\n final: false,\n });\n target.lastFlushAt = now();\n target.dirty = false;\n } else {\n target.dirty = true;\n }\n ctx.state.set(state);\n };\n\n const finish = async (ctx: ConversationProjectionContextIface): Promise<void> => {\n const state = projectionStateOf(ctx.state.get());\n if (state.messageId != null) {\n await finishMessage(ctx, state, visibleContent(state));\n return;\n }\n for (const segment of state.segments) {\n await finishMessage(ctx, segment, visibleContent({ segments: [segment] }));\n }\n };\n\n const finishMessage = async (\n ctx: ConversationProjectionContextIface,\n state: z.infer<typeof projectedMessageSchema>,\n content: ContentBlock[],\n ): Promise<void> => {\n if (state.messageId == null) {\n return;\n }\n await ctx.platform.conversations.updateMessage({\n messageId: state.messageId,\n content,\n final: true,\n });\n // The finished reply is routed like any message: other agents may observe\n // it or, when mentioned within the hop limit, be prompted.\n const reply =\n state.messageSeq == null\n ? null\n : (\n await ctx.platform.conversations.recentMessages({\n conversationId: ctx.conversation.conversationId,\n beforeSeq: state.messageSeq + 1,\n limit: 1,\n })\n ).find((message) => message.messageId === state.messageId);\n if (reply == null) {\n ctx.platform.log('finished reply not found for routing', {\n messageId: state.messageId,\n seq: state.messageSeq,\n });\n }\n if (reply != null) {\n if (ctx.platform.conversations.dispatchMessage == null)\n throw new Error('channel dispatch is unavailable');\n await ctx.platform.conversations.dispatchMessage({\n conversationId: reply.conversationId,\n messageId: reply.messageId,\n });\n }\n };\n\n const project: ProjectorIface['project'] = async (update, ctx) => {\n const { conversation, agentParticipant } = ctx;\n if (conversation == null || agentParticipant == null)\n throw new Error('Final rendering requires a Final conversation');\n const context = { ...ctx, conversation, agentParticipant };\n return turnEndOf(update) == null ? fold(update, context) : finish(context);\n };\n\n return { project };\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["segment"]
|
|
7
7
|
}
|
|
@@ -10,8 +10,9 @@ export declare const computerWorkspaceName: (options: {
|
|
|
10
10
|
target: string;
|
|
11
11
|
conversationWorkspace: boolean;
|
|
12
12
|
}) => Promise<string>;
|
|
13
|
-
export type { ComputersApiConfigIface
|
|
14
|
-
export {
|
|
13
|
+
export type { ComputersApiConfigIface } from "./computers-api.mjs";
|
|
14
|
+
export type { FinalPlatformAccessIface } from '@finalai/agent-provider/sandbox';
|
|
15
|
+
export { COMPUTER_SANDBOX_SPECIFIER } from "./environment.mjs";
|
|
15
16
|
export { computeComputerArtifactKey } from "./guide.mjs";
|
|
16
17
|
export type { ComputerGuideVersionIface } from "./guide.mjs";
|
|
17
18
|
export type { RepositoryCloneSourceIface } from "./workspace-setup.mjs";
|
|
@@ -19,7 +20,7 @@ export interface ComputerSandboxLogIface {
|
|
|
19
20
|
(message: string, fields?: Record<string, string | number | boolean | null>): void;
|
|
20
21
|
}
|
|
21
22
|
export interface ComputerSandboxProviderConfigIface {
|
|
22
|
-
computersApi
|
|
23
|
+
computersApi?: ComputersApiConfigIface;
|
|
23
24
|
networking?: SandboxNetworkingExtensionIface;
|
|
24
25
|
registryCredentialId?: string;
|
|
25
26
|
defaultImage?: string;
|
|
@@ -46,7 +46,6 @@ import { computerEditor } from "./computer-editor.mjs";
|
|
|
46
46
|
import { readComputerSnapshot } from "./computer-snapshots.mjs";
|
|
47
47
|
import {
|
|
48
48
|
createComputersApiClient,
|
|
49
|
-
createComputersApiCredentials,
|
|
50
49
|
isComputersApiError,
|
|
51
50
|
isComputersApiStatus
|
|
52
51
|
} from "./computers-api.mjs";
|
|
@@ -75,11 +74,7 @@ import {
|
|
|
75
74
|
WORKSPACE_ROOT
|
|
76
75
|
} from "./workspace-setup.mjs";
|
|
77
76
|
const computerWorkspaceName = async (options) => options.conversationWorkspace && options.conversationId != null ? `ac-${(await sha256Hex(`${options.conversationId}:${options.target}`)).slice(0, 24)}` : `ab-${compactId(options.bindingId)}`;
|
|
78
|
-
import {
|
|
79
|
-
COMPUTER_SANDBOX_SPECIFIER as COMPUTER_SANDBOX_SPECIFIER2,
|
|
80
|
-
COMPUTERS_API_KEY_SECRET,
|
|
81
|
-
COMPUTERS_API_TEAM_SECRET
|
|
82
|
-
} from "./environment.mjs";
|
|
77
|
+
import { COMPUTER_SANDBOX_SPECIFIER as COMPUTER_SANDBOX_SPECIFIER2 } from "./environment.mjs";
|
|
83
78
|
import { computeComputerArtifactKey as computeComputerArtifactKey2 } from "./guide.mjs";
|
|
84
79
|
const computerArtifactSchema = sandboxSeedArtifactSchema.extend({
|
|
85
80
|
version: z.literal(1),
|
|
@@ -180,9 +175,8 @@ const createComputerSandboxProvider = (config) => {
|
|
|
180
175
|
const now = config.now ?? (() => Date.now());
|
|
181
176
|
const sleep = config.sleep ?? defaultSleep;
|
|
182
177
|
const defaultImage = config.defaultImage ?? DEFAULT_COMPUTER_IMAGE;
|
|
183
|
-
const credentials = createComputersApiCredentials(config.computersApi, now);
|
|
184
178
|
const runningWaitMs = config.runningWaitMs ?? 5 * 6e4;
|
|
185
|
-
const clientFor = (
|
|
179
|
+
const clientFor = (credentials) => createComputersApiClient({ config: config.computersApi ?? {}, credentials });
|
|
186
180
|
const registerImage = (client, reference) => client.registerImage(
|
|
187
181
|
config.registryCredentialId == null ? { reference } : { reference, registryCredentialId: config.registryCredentialId }
|
|
188
182
|
);
|
|
@@ -627,6 +621,7 @@ ${commandHelp}`}`,
|
|
|
627
621
|
platformEgressHosts: args.ctx.platformEgressHosts,
|
|
628
622
|
platformNetworkPolicy: args.ctx.platformNetworkPolicy,
|
|
629
623
|
organizationId: args.ctx.session.organizationId,
|
|
624
|
+
credentials: args.ctx.credentials,
|
|
630
625
|
actingUser: args.ctx.actingUser,
|
|
631
626
|
bindings: args.ctx.bindings
|
|
632
627
|
},
|
|
@@ -664,6 +659,7 @@ ${commandHelp}`}`,
|
|
|
664
659
|
platformEgressHosts: args.ctx.platformEgressHosts,
|
|
665
660
|
platformNetworkPolicy: args.ctx.platformNetworkPolicy,
|
|
666
661
|
organizationId: args.ctx.session.organizationId,
|
|
662
|
+
credentials: args.ctx.credentials,
|
|
667
663
|
actingUser: args.ctx.actingUser,
|
|
668
664
|
bindings: args.ctx.bindings
|
|
669
665
|
},
|
|
@@ -678,6 +674,7 @@ ${commandHelp}`}`,
|
|
|
678
674
|
ctx: {
|
|
679
675
|
platformEgressHosts: ctx.platformEgressHosts,
|
|
680
676
|
organizationId: ctx.session.organizationId,
|
|
677
|
+
credentials: ctx.credentials,
|
|
681
678
|
actingUser: ctx.actingUser,
|
|
682
679
|
bindings: ctx.bindings
|
|
683
680
|
},
|
|
@@ -715,7 +712,7 @@ ${commandHelp}`}`,
|
|
|
715
712
|
const results = await Promise.allSettled([
|
|
716
713
|
(async () => {
|
|
717
714
|
if (managed && args.fence) {
|
|
718
|
-
const client = clientFor(args.ctx.
|
|
715
|
+
const client = clientFor(args.ctx.credentials);
|
|
719
716
|
let mayFence = args.policySaved || savedPolicyWorkspaces.has(args.ctx.session.organizationId + ":" + workspaceId);
|
|
720
717
|
try {
|
|
721
718
|
if (config.networking != null) {
|
|
@@ -762,6 +759,7 @@ ${commandHelp}`}`,
|
|
|
762
759
|
await config.networking.suspend({
|
|
763
760
|
ctx: {
|
|
764
761
|
organizationId: args.ctx.session.organizationId,
|
|
762
|
+
credentials: args.ctx.credentials,
|
|
765
763
|
actingUser: args.ctx.actingUser,
|
|
766
764
|
bindings: args.ctx.bindings
|
|
767
765
|
},
|
|
@@ -832,7 +830,7 @@ ${commandHelp}`}`,
|
|
|
832
830
|
}
|
|
833
831
|
};
|
|
834
832
|
const build = async (ctx, spec, key) => {
|
|
835
|
-
const client = clientFor(ctx.
|
|
833
|
+
const client = clientFor(ctx.credentials);
|
|
836
834
|
const diskGb = config.machine?.diskGb ?? null;
|
|
837
835
|
const imageVersionId = await readyImageVersion(client, {
|
|
838
836
|
image: spec.image,
|
|
@@ -965,7 +963,7 @@ ${step.run}`,
|
|
|
965
963
|
});
|
|
966
964
|
};
|
|
967
965
|
const awaitRunning = (ctx, rawState, options2) => awaitComputerRunning(
|
|
968
|
-
clientFor(ctx.
|
|
966
|
+
clientFor(ctx.credentials),
|
|
969
967
|
computerStateSchema.pick({ computerId: true }).parse(rawState).computerId,
|
|
970
968
|
options2.timeoutMs
|
|
971
969
|
);
|
|
@@ -974,10 +972,7 @@ ${step.run}`,
|
|
|
974
972
|
const state = computerStateSchema.parse(rawState);
|
|
975
973
|
if (request.operation === ProcessInspectionOperation.Stop && state.access === SandboxAccess.Read)
|
|
976
974
|
throw new Error("stopping processes requires execute access");
|
|
977
|
-
const result = await clientFor(ctx.
|
|
978
|
-
state.computerId,
|
|
979
|
-
request
|
|
980
|
-
);
|
|
975
|
+
const result = await clientFor(ctx.credentials).inspectProcesses(state.computerId, request);
|
|
981
976
|
return request.operation === ProcessInspectionOperation.Available && "available" in result ? { ...result, canStop: state.access !== SandboxAccess.Read } : result;
|
|
982
977
|
},
|
|
983
978
|
editor: async (ctx, rawState, request) => {
|
|
@@ -986,7 +981,7 @@ ${step.run}`,
|
|
|
986
981
|
if (request.operation === "available") return { available: false };
|
|
987
982
|
throw new Error("editor requires write access");
|
|
988
983
|
}
|
|
989
|
-
const client = clientFor(ctx.
|
|
984
|
+
const client = clientFor(ctx.credentials);
|
|
990
985
|
const open = (expiresAtMs) => computerEditor({
|
|
991
986
|
client,
|
|
992
987
|
computerId: state.computerId,
|
|
@@ -1050,7 +1045,7 @@ ${step.run}`,
|
|
|
1050
1045
|
readSnapshot: async (ctx, rawState, request) => {
|
|
1051
1046
|
const state = computerStateSchema.parse(rawState);
|
|
1052
1047
|
return readComputerSnapshot({
|
|
1053
|
-
client: clientFor(ctx.
|
|
1048
|
+
client: clientFor(ctx.credentials),
|
|
1054
1049
|
computerId: state.computerId,
|
|
1055
1050
|
request
|
|
1056
1051
|
});
|
|
@@ -1073,13 +1068,10 @@ ${step.run}`,
|
|
|
1073
1068
|
if (prepared?.key === baseKey && prepared.providerRef === formatProviderRef(packageRef(COMPUTER_SANDBOX_SPECIFIER))) {
|
|
1074
1069
|
const cached2 = computerArtifactSchema.safeParse(prepared.artifact);
|
|
1075
1070
|
if (cached2.success && cached2.data.key === baseKey && cached2.data.base.kind === "captured") {
|
|
1076
|
-
const base = await clientFor(ctx.
|
|
1071
|
+
const base = await clientFor(ctx.credentials).getBase(cached2.data.base.baseId);
|
|
1077
1072
|
if (base?.state === ComputerBaseState.Ready) {
|
|
1078
1073
|
if (!base.retained) {
|
|
1079
|
-
const retained = await clientFor(ctx.
|
|
1080
|
-
base.id,
|
|
1081
|
-
true
|
|
1082
|
-
);
|
|
1074
|
+
const retained = await clientFor(ctx.credentials).setBaseRetention(base.id, true);
|
|
1083
1075
|
if (!retained.retained)
|
|
1084
1076
|
throw computerSandboxError(
|
|
1085
1077
|
"build_failed",
|
|
@@ -1092,13 +1084,10 @@ ${step.run}`,
|
|
|
1092
1084
|
}
|
|
1093
1085
|
const cached = computerArtifactSchema.safeParse(await ctx.artifacts?.get(baseKey));
|
|
1094
1086
|
if (cached.success && cached.data.key === baseKey && cached.data.base.kind === "captured") {
|
|
1095
|
-
const base = await clientFor(ctx.
|
|
1087
|
+
const base = await clientFor(ctx.credentials).getBase(cached.data.base.baseId);
|
|
1096
1088
|
if (base?.state === ComputerBaseState.Ready) {
|
|
1097
1089
|
if (!base.retained) {
|
|
1098
|
-
const retained = await clientFor(ctx.
|
|
1099
|
-
base.id,
|
|
1100
|
-
true
|
|
1101
|
-
);
|
|
1090
|
+
const retained = await clientFor(ctx.credentials).setBaseRetention(base.id, true);
|
|
1102
1091
|
if (!retained.retained)
|
|
1103
1092
|
throw computerSandboxError(
|
|
1104
1093
|
"build_failed",
|
|
@@ -1112,7 +1101,7 @@ ${step.run}`,
|
|
|
1112
1101
|
await ctx.artifacts?.put(baseKey, built);
|
|
1113
1102
|
return { ...built, key, ...seedArtifact };
|
|
1114
1103
|
}
|
|
1115
|
-
const imageVersionId = spec.image.startsWith(LINEAGE_PREFIX) ? null : (await registerImage(clientFor(ctx.
|
|
1104
|
+
const imageVersionId = spec.image.startsWith(LINEAGE_PREFIX) ? null : (await registerImage(clientFor(ctx.credentials), spec.image)).id;
|
|
1116
1105
|
ctx.log?.(`computer base ${key} is image ${spec.image}`);
|
|
1117
1106
|
return {
|
|
1118
1107
|
version: 1,
|
|
@@ -1146,7 +1135,7 @@ ${step.run}`,
|
|
|
1146
1135
|
);
|
|
1147
1136
|
}
|
|
1148
1137
|
const env = platformProcessEnvironment(ctx, await loadVariables(ctx, spec));
|
|
1149
|
-
const client = clientFor(ctx.
|
|
1138
|
+
const client = clientFor(ctx.credentials);
|
|
1150
1139
|
const workspace = spec.parameters.workspace;
|
|
1151
1140
|
const name = await computerWorkspaceName({
|
|
1152
1141
|
...ctx.session,
|
|
@@ -1366,7 +1355,7 @@ ${step.run}`,
|
|
|
1366
1355
|
ctx = { ...ctx, session: { ...ctx.session, workspaceId: state.workspaceId } };
|
|
1367
1356
|
if (state.networkPolicySaved === true && state.workspaceId != null && ctx.session.workspaceId !== state.workspaceId)
|
|
1368
1357
|
throw new Error("Passive workspace identity does not match retained provider state");
|
|
1369
|
-
const client = clientFor(ctx.
|
|
1358
|
+
const client = clientFor(ctx.credentials);
|
|
1370
1359
|
const info = await client.getComputer(state.computerId);
|
|
1371
1360
|
if (info == null || info.state !== ComputerState.Running)
|
|
1372
1361
|
throw computerSandboxError(
|
|
@@ -1409,7 +1398,7 @@ ${step.run}`,
|
|
|
1409
1398
|
})
|
|
1410
1399
|
}
|
|
1411
1400
|
};
|
|
1412
|
-
const client = clientFor(ctx.
|
|
1401
|
+
const client = clientFor(ctx.credentials);
|
|
1413
1402
|
const workspaceId = sandboxWorkspaceId({
|
|
1414
1403
|
session: ctx.session,
|
|
1415
1404
|
workspace: sandboxWorkspaceOf[state.workspace]
|
|
@@ -1579,7 +1568,7 @@ ${step.run}`,
|
|
|
1579
1568
|
failures.push(cause);
|
|
1580
1569
|
}
|
|
1581
1570
|
try {
|
|
1582
|
-
const client = clientFor(ctx.
|
|
1571
|
+
const client = clientFor(ctx.credentials);
|
|
1583
1572
|
let current;
|
|
1584
1573
|
try {
|
|
1585
1574
|
current = await client.getComputer(state.computerId);
|
|
@@ -1621,7 +1610,7 @@ ${step.run}`,
|
|
|
1621
1610
|
},
|
|
1622
1611
|
onSessionEnd: async (ctx, _artifact, rawState, { reason }) => {
|
|
1623
1612
|
const state = computerStateSchema.parse(rawState);
|
|
1624
|
-
const client = clientFor(ctx.
|
|
1613
|
+
const client = clientFor(ctx.credentials);
|
|
1625
1614
|
const finish = async () => {
|
|
1626
1615
|
const action = state.workspace === ComputerWorkspacePolicy.Conversation ? "stop" : "delete";
|
|
1627
1616
|
log("computer_sandbox.session_end", { computerId: state.computerId, reason, action });
|
|
@@ -1657,8 +1646,6 @@ ${step.run}`,
|
|
|
1657
1646
|
});
|
|
1658
1647
|
};
|
|
1659
1648
|
export {
|
|
1660
|
-
COMPUTERS_API_KEY_SECRET,
|
|
1661
|
-
COMPUTERS_API_TEAM_SECRET,
|
|
1662
1649
|
COMPUTER_SANDBOX_SPECIFIER2 as COMPUTER_SANDBOX_SPECIFIER,
|
|
1663
1650
|
computeComputerArtifactKey2 as computeComputerArtifactKey,
|
|
1664
1651
|
computerWorkspaceName,
|