@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.10
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/package.json +12 -3
- package/src/adapter/cloudflare/index.ts +56 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/db/agent-tool.repo.ts +27 -0
- package/src/db/index.ts +33 -0
- package/src/db/interaction.repo.ts +185 -0
- package/src/db/schema.ts +25 -1
- package/src/db/submission.repo.ts +63 -1
- package/src/index.ts +57 -21
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +73 -9
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +104 -6
- package/src/kernel/runtime-assembly-view.ts +37 -0
- package/src/kernel/runtime-assembly.ts +41 -0
- package/src/kernel/runtime-config.ts +4 -0
- package/src/kernel/runtime-load.ts +191 -0
- package/src/kernel/state.ts +12 -1
- package/src/kernel/submission-lifecycle.ts +33 -2
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/lib/mcp.ts +7 -3
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/context.ts +3 -3
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +6 -3
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +26 -31
- package/src/pi/runtime-adapter/execution.ts +198 -15
- package/src/pi/runtime-adapter/index.ts +24 -8
- package/src/pi/runtime-adapter/models.ts +382 -35
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +190 -12
- package/src/pi/tool/compiler.ts +34 -1
- package/src/pi/tool/core-host.ts +19 -24
- package/src/pi/tool/core.ts +30 -120
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +2 -0
- package/src/pi/tool/mcp.ts +96 -68
- package/src/pi/tool/schedule.ts +41 -19
- package/src/pi/tool/skill.ts +126 -420
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +281 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +105 -263
- package/src/pi/turn/index.ts +20 -0
- package/src/pi/turn/interaction.ts +181 -0
- package/src/pi/turn/tool-recovery.ts +244 -1
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +568 -321
- package/src/{plugins.ts → runtime-assembler.ts} +372 -398
- package/src/runtime-definition.ts +175 -0
- package/src/runtime.ts +835 -204
- package/src/tool-registry.ts +143 -0
- package/src/workspace-versioning.ts +46 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { Sandbox } from "@cloudflare/sandbox";
|
|
2
|
+
import type {
|
|
3
|
+
ChatCapableAgentClass,
|
|
4
|
+
RunAgentToolOptions,
|
|
5
|
+
} from "agents";
|
|
6
|
+
import type {
|
|
7
|
+
RuntimeBrowserPort,
|
|
8
|
+
RuntimeMemoryPort,
|
|
9
|
+
RuntimePlatformPort,
|
|
10
|
+
RuntimeSubagentPort,
|
|
11
|
+
WorkspacePort,
|
|
12
|
+
} from "../../../kernel/bindings";
|
|
13
|
+
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
14
|
+
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
15
|
+
import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
|
|
16
|
+
import type { RuntimeAgentConfigContext } from "../../../runtime-agent-context";
|
|
17
|
+
import { createWorkspaceCodeExecutionPort } from "../../../pi/tool/core-host";
|
|
18
|
+
import {
|
|
19
|
+
createCloudflareSandboxAdapter,
|
|
20
|
+
type SandboxAdmission,
|
|
21
|
+
type SandboxObserver,
|
|
22
|
+
} from "../sandbox/adapter";
|
|
23
|
+
import { sandboxIdForSession } from "../sandbox/id";
|
|
24
|
+
import type { WorkspacePublishPort } from "../workspace/publisher";
|
|
25
|
+
|
|
26
|
+
export type WorkspaceLoader = () => Promise<{
|
|
27
|
+
value: WorkspacePort | undefined;
|
|
28
|
+
degradations: readonly RuntimeDegradation[];
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type PlatformLoader = () => Promise<RuntimePlatformPort>;
|
|
32
|
+
|
|
33
|
+
export interface CloudflarePlatformBindings {
|
|
34
|
+
LOADER: WorkerLoader;
|
|
35
|
+
BROWSER?: RuntimeBrowserPort;
|
|
36
|
+
TELEMETRY_CONSOLE?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkspaceRequirement {
|
|
40
|
+
memoryEnabled?: boolean;
|
|
41
|
+
sandboxEnabled?: boolean;
|
|
42
|
+
deniedToolNames?: readonly string[];
|
|
43
|
+
skillWorkspaceModes?: readonly ("none" | "read" | "read-write")[];
|
|
44
|
+
extensionWorkspaceModes?: readonly ("none" | "read" | "read-write")[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_MEMORY: RuntimeMemoryProfile = Object.freeze({
|
|
48
|
+
enabled: true,
|
|
49
|
+
memoryTokens: 2_000,
|
|
50
|
+
preferencesTokens: 500,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const WORKSPACE_TOOL_NAMES = [
|
|
54
|
+
"read",
|
|
55
|
+
"write",
|
|
56
|
+
"edit",
|
|
57
|
+
"list",
|
|
58
|
+
"find",
|
|
59
|
+
"grep",
|
|
60
|
+
"delete",
|
|
61
|
+
"bash",
|
|
62
|
+
"execute",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
export function workspaceRequired(input: WorkspaceRequirement): boolean {
|
|
66
|
+
const denied = new Set(input.deniedToolNames ?? []);
|
|
67
|
+
return input.memoryEnabled !== false ||
|
|
68
|
+
input.sandboxEnabled === true ||
|
|
69
|
+
input.skillWorkspaceModes?.some((mode) => mode !== "none") === true ||
|
|
70
|
+
input.extensionWorkspaceModes?.some((mode) => mode !== "none") === true ||
|
|
71
|
+
WORKSPACE_TOOL_NAMES.some((name) => !denied.has(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createWorkspaceLoader(
|
|
75
|
+
load: () => WorkspacePort,
|
|
76
|
+
): WorkspaceLoader {
|
|
77
|
+
let cached: ReturnType<WorkspaceLoader> | undefined;
|
|
78
|
+
return () => cached ??= Promise.resolve().then(() => {
|
|
79
|
+
try {
|
|
80
|
+
const workspace = load();
|
|
81
|
+
return {
|
|
82
|
+
value: workspace,
|
|
83
|
+
degradations: [],
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
value: undefined,
|
|
88
|
+
degradations: [{
|
|
89
|
+
capability: "workspace" as const,
|
|
90
|
+
reason: "unavailable" as const,
|
|
91
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
92
|
+
}],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createPlatformLoader<
|
|
99
|
+
Env extends Cloudflare.Env & CloudflarePlatformBindings,
|
|
100
|
+
>(context: RuntimeAgentConfigContext<Env>): PlatformLoader {
|
|
101
|
+
let cached: ReturnType<PlatformLoader> | undefined;
|
|
102
|
+
return () => cached ??= Promise.resolve().then(() => {
|
|
103
|
+
const exports = (context.ctx as unknown as {
|
|
104
|
+
exports: {
|
|
105
|
+
HttpGateway(options: Record<string, never>): Fetcher;
|
|
106
|
+
};
|
|
107
|
+
}).exports;
|
|
108
|
+
return {
|
|
109
|
+
loader: context.env.LOADER,
|
|
110
|
+
...(context.env.BROWSER ? { browser: context.env.BROWSER } : {}),
|
|
111
|
+
outbound: () => exports.HttpGateway({}),
|
|
112
|
+
telemetryConsole: context.env.TELEMETRY_CONSOLE === "1",
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function prepareWorkspace<Env extends Cloudflare.Env>(
|
|
118
|
+
context: RuntimeAgentConfigContext<Env>,
|
|
119
|
+
workspaceLoader: WorkspaceLoader,
|
|
120
|
+
platformLoader: PlatformLoader,
|
|
121
|
+
) {
|
|
122
|
+
const [workspace, platform] = await Promise.all([
|
|
123
|
+
workspaceLoader(),
|
|
124
|
+
platformLoader(),
|
|
125
|
+
]);
|
|
126
|
+
if (!workspace.value) return { degradations: workspace.degradations };
|
|
127
|
+
return {
|
|
128
|
+
workspace: workspace.value,
|
|
129
|
+
codeExecution: createWorkspaceCodeExecutionPort({
|
|
130
|
+
ctx: context.ctx,
|
|
131
|
+
loader: platform.loader,
|
|
132
|
+
outbound: platform.outbound(),
|
|
133
|
+
workspace: workspace.value,
|
|
134
|
+
}),
|
|
135
|
+
degradations: workspace.degradations,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function prepareMemory(
|
|
140
|
+
configured: Partial<RuntimeMemoryProfile> | null | undefined,
|
|
141
|
+
workspaceLoader: WorkspaceLoader,
|
|
142
|
+
port: RuntimeMemoryPort,
|
|
143
|
+
): Promise<{
|
|
144
|
+
memoryProfile: RuntimeMemoryProfile;
|
|
145
|
+
memory?: RuntimeMemoryPort;
|
|
146
|
+
degradations: RuntimeDegradation[];
|
|
147
|
+
}> {
|
|
148
|
+
const memoryConfig = configured ?? {};
|
|
149
|
+
if (memoryConfig.enabled === false) {
|
|
150
|
+
return {
|
|
151
|
+
memoryProfile: { ...DEFAULT_MEMORY, ...memoryConfig },
|
|
152
|
+
degradations: [{
|
|
153
|
+
capability: "memory" as const,
|
|
154
|
+
reason: "not_configured" as const,
|
|
155
|
+
}],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const workspace = (await workspaceLoader()).value;
|
|
159
|
+
const memoryProfile = {
|
|
160
|
+
...DEFAULT_MEMORY,
|
|
161
|
+
...memoryConfig,
|
|
162
|
+
enabled: workspace !== undefined,
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
memoryProfile,
|
|
166
|
+
...(memoryProfile.enabled ? { memory: port } : {}),
|
|
167
|
+
degradations: memoryProfile.enabled
|
|
168
|
+
? []
|
|
169
|
+
: [{
|
|
170
|
+
capability: "memory" as const,
|
|
171
|
+
reason: "unavailable" as const,
|
|
172
|
+
}],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function prepareSubagents<Env extends Cloudflare.Env>(
|
|
177
|
+
context: RuntimeAgentConfigContext<Env>,
|
|
178
|
+
requested: readonly string[],
|
|
179
|
+
agentClass: ChatCapableAgentClass,
|
|
180
|
+
) {
|
|
181
|
+
const enabledSubagents: string[] = [];
|
|
182
|
+
const degradations: RuntimeDegradation[] = [];
|
|
183
|
+
|
|
184
|
+
for (const name of new Set(requested.filter(Boolean))) {
|
|
185
|
+
if (name in AGENT_TYPES.byName) enabledSubagents.push(name);
|
|
186
|
+
else {
|
|
187
|
+
degradations.push({
|
|
188
|
+
capability: "subagent",
|
|
189
|
+
reason: "not_deployed",
|
|
190
|
+
detail: name,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const subagents: RuntimeSubagentPort = {
|
|
196
|
+
run: async (agentType, input, options) => {
|
|
197
|
+
const runOptions: RunAgentToolOptions<unknown> = {
|
|
198
|
+
input: { agentType, ...input },
|
|
199
|
+
};
|
|
200
|
+
if (options?.detached) {
|
|
201
|
+
runOptions.detached = { maxBudgetMs: options.maxBudgetMs };
|
|
202
|
+
if (options.notifySource) {
|
|
203
|
+
runOptions.detached.notify = { source: options.notifySource };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const result = await context.runAgentTool(agentClass, runOptions);
|
|
207
|
+
return {
|
|
208
|
+
status: result.status,
|
|
209
|
+
runId: result.runId,
|
|
210
|
+
output: result.output,
|
|
211
|
+
error: result.error,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
enabledSubagents,
|
|
218
|
+
...(enabledSubagents.length > 0 ? { subagents } : {}),
|
|
219
|
+
degradations,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function prepareSandbox(options: {
|
|
224
|
+
enabled: boolean;
|
|
225
|
+
deploymentEnabled: boolean;
|
|
226
|
+
namespace?: DurableObjectNamespace<Sandbox>;
|
|
227
|
+
userId: string;
|
|
228
|
+
sessionId: string;
|
|
229
|
+
workspaceLoader: WorkspaceLoader;
|
|
230
|
+
publisher: WorkspacePublishPort;
|
|
231
|
+
onPublishComplete?: () => Promise<void>;
|
|
232
|
+
admission?: SandboxAdmission;
|
|
233
|
+
observer?: SandboxObserver;
|
|
234
|
+
}) {
|
|
235
|
+
if (!options.enabled) return { degradations: [] };
|
|
236
|
+
if (!options.deploymentEnabled) {
|
|
237
|
+
return {
|
|
238
|
+
degradations: [{
|
|
239
|
+
capability: "sandbox" as const,
|
|
240
|
+
reason: "disabled_by_deployment" as const,
|
|
241
|
+
}],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (!options.namespace) {
|
|
245
|
+
return {
|
|
246
|
+
degradations: [{
|
|
247
|
+
capability: "sandbox" as const,
|
|
248
|
+
reason: "unavailable" as const,
|
|
249
|
+
detail: "sandbox_binding_missing",
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const workspace = (await options.workspaceLoader()).value;
|
|
254
|
+
if (!workspace) {
|
|
255
|
+
return {
|
|
256
|
+
degradations: [{
|
|
257
|
+
capability: "sandbox" as const,
|
|
258
|
+
reason: "unavailable" as const,
|
|
259
|
+
detail: "persistent_workspace_unavailable",
|
|
260
|
+
}],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
sandbox: createCloudflareSandboxAdapter({
|
|
265
|
+
sandboxNamespace: options.namespace,
|
|
266
|
+
sandboxId: await sandboxIdForSession(options.userId, options.sessionId),
|
|
267
|
+
workspace,
|
|
268
|
+
publisher: options.publisher,
|
|
269
|
+
...(options.onPublishComplete
|
|
270
|
+
? { onPublishComplete: options.onPublishComplete }
|
|
271
|
+
: {}),
|
|
272
|
+
...(options.admission ? { admission: options.admission } : {}),
|
|
273
|
+
...(options.observer ? { observer: options.observer } : {}),
|
|
274
|
+
}),
|
|
275
|
+
degradations: [],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RuntimeCodeExecutionPort,
|
|
3
|
+
RuntimeMemoryPort,
|
|
4
|
+
RuntimeSandboxPort,
|
|
5
|
+
RuntimeSchedulePort,
|
|
6
|
+
RuntimeSubagentPort,
|
|
7
|
+
WorkspacePort,
|
|
8
|
+
} from "../../../kernel/bindings";
|
|
9
|
+
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
10
|
+
import type { RuntimeMemoryProfile } from "../../../kernel/profile";
|
|
11
|
+
import type { WorkspaceRevisionRestorePort } from "../../../workspace-versioning";
|
|
12
|
+
import { createCodeExecutionTool } from "../../../pi/tool/core";
|
|
13
|
+
import { createScheduleTools } from "../../../pi/tool/schedule";
|
|
14
|
+
import { createSubagentTools } from "../../../pi/tool/subagent";
|
|
15
|
+
import { createWorkspaceRevisionTools } from "../../../pi/tool/workspace-revision";
|
|
16
|
+
import {
|
|
17
|
+
createSandboxTools,
|
|
18
|
+
createWorkspaceTools,
|
|
19
|
+
} from "../../../pi/tool/workspace-sandbox";
|
|
20
|
+
import {
|
|
21
|
+
mergeToolRegistries,
|
|
22
|
+
type ToolAssemblyResult,
|
|
23
|
+
type ToolRegistry,
|
|
24
|
+
} from "../../../tool-registry";
|
|
25
|
+
|
|
26
|
+
export function selectTools(
|
|
27
|
+
tools: ToolRegistry,
|
|
28
|
+
names: Iterable<string>,
|
|
29
|
+
): ToolRegistry {
|
|
30
|
+
const selected = new Set(names);
|
|
31
|
+
return Object.fromEntries(
|
|
32
|
+
Object.entries(tools).filter(([name]) => selected.has(name)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assembleUniversalAgentTools(options: {
|
|
37
|
+
hostTools?: ToolRegistry;
|
|
38
|
+
workspace?: WorkspacePort;
|
|
39
|
+
workspaceRevisions?: WorkspaceRevisionRestorePort;
|
|
40
|
+
codeExecution?: RuntimeCodeExecutionPort;
|
|
41
|
+
sandbox?: RuntimeSandboxPort;
|
|
42
|
+
schedule?: RuntimeSchedulePort;
|
|
43
|
+
subagents?: RuntimeSubagentPort;
|
|
44
|
+
enabledSubagents?: readonly string[];
|
|
45
|
+
memory?: RuntimeMemoryPort;
|
|
46
|
+
memoryProfile: RuntimeMemoryProfile;
|
|
47
|
+
degradations?: readonly RuntimeDegradation[];
|
|
48
|
+
dispose?: () => Promise<void>;
|
|
49
|
+
}): ToolAssemblyResult {
|
|
50
|
+
const tools = mergeToolRegistries(
|
|
51
|
+
options.hostTools ?? {},
|
|
52
|
+
options.workspace ? createWorkspaceTools(options.workspace) : {},
|
|
53
|
+
options.workspaceRevisions
|
|
54
|
+
? createWorkspaceRevisionTools(options.workspaceRevisions)
|
|
55
|
+
: {},
|
|
56
|
+
options.codeExecution
|
|
57
|
+
? createCodeExecutionTool(options.codeExecution)
|
|
58
|
+
: {},
|
|
59
|
+
options.sandbox ? createSandboxTools(options.sandbox) : {},
|
|
60
|
+
options.schedule ? createScheduleTools(options.schedule) : {},
|
|
61
|
+
options.subagents
|
|
62
|
+
? createSubagentTools(
|
|
63
|
+
options.subagents,
|
|
64
|
+
options.enabledSubagents ?? [],
|
|
65
|
+
)
|
|
66
|
+
: {},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
tools,
|
|
71
|
+
bindings: {
|
|
72
|
+
...(options.workspace ? { workspace: options.workspace } : {}),
|
|
73
|
+
...(options.memory ? { memory: options.memory } : {}),
|
|
74
|
+
},
|
|
75
|
+
enabledSubagents: options.enabledSubagents ?? [],
|
|
76
|
+
memoryProfile: options.memoryProfile,
|
|
77
|
+
degradations: options.degradations ?? [],
|
|
78
|
+
...(options.dispose ? { dispose: options.dispose } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from @cloudflare/shell.
|
|
3
|
+
* MIT License Copyright (c) 2025 Cloudflare, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
* in the Software without restriction, including without limitation the rights
|
|
8
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
* furnished to do so, subject to the following conditions:
|
|
11
|
+
*
|
|
12
|
+
* The above copyright notice and this permission notice shall be included in
|
|
13
|
+
* all copies or substantial portions of the Software.
|
|
14
|
+
*
|
|
15
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
* SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
import type {
|
|
24
|
+
FsClient,
|
|
25
|
+
PromiseFsClient,
|
|
26
|
+
} from "isomorphic-git";
|
|
27
|
+
import type {
|
|
28
|
+
WorkspaceFileInfo,
|
|
29
|
+
WorkspacePort,
|
|
30
|
+
} from "../../../kernel/bindings";
|
|
31
|
+
|
|
32
|
+
const PAGE_SIZE = 256;
|
|
33
|
+
|
|
34
|
+
class GitStat {
|
|
35
|
+
readonly size: number;
|
|
36
|
+
readonly mtime: Date;
|
|
37
|
+
readonly mtimeMs: number;
|
|
38
|
+
readonly ctimeMs: number;
|
|
39
|
+
readonly mode: number;
|
|
40
|
+
readonly ino = 0;
|
|
41
|
+
readonly uid = 0;
|
|
42
|
+
readonly gid = 0;
|
|
43
|
+
readonly dev = 0;
|
|
44
|
+
|
|
45
|
+
constructor(private readonly info: WorkspaceFileInfo) {
|
|
46
|
+
this.size = info.size;
|
|
47
|
+
this.mtime = new Date(info.updatedAt);
|
|
48
|
+
this.mtimeMs = info.updatedAt;
|
|
49
|
+
this.ctimeMs = info.createdAt;
|
|
50
|
+
this.mode = info.type === "directory"
|
|
51
|
+
? 0o40755
|
|
52
|
+
: info.type === "symlink"
|
|
53
|
+
? 0o120000
|
|
54
|
+
: 0o100644;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isFile() {
|
|
58
|
+
return this.info.type === "file";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isDirectory() {
|
|
62
|
+
return this.info.type === "directory";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
isSymbolicLink() {
|
|
66
|
+
return this.info.type === "symlink";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fsError(path: string, cause?: unknown): Error & { code: string } {
|
|
71
|
+
if (
|
|
72
|
+
cause instanceof Error &&
|
|
73
|
+
"code" in cause &&
|
|
74
|
+
typeof (cause as { code: unknown }).code === "string"
|
|
75
|
+
) return cause as Error & { code: string };
|
|
76
|
+
|
|
77
|
+
const error = new Error(
|
|
78
|
+
cause instanceof Error ? cause.message : `ENOENT: ${path}`,
|
|
79
|
+
) as Error & { code: string };
|
|
80
|
+
error.code = cause === undefined ? "ENOENT" : "EIO";
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readAll(workspace: WorkspacePort, path: string) {
|
|
85
|
+
const entries: WorkspaceFileInfo[] = [];
|
|
86
|
+
let offset = 0;
|
|
87
|
+
while (true) {
|
|
88
|
+
const page = await workspace.readDir(path, { limit: PAGE_SIZE, offset });
|
|
89
|
+
if (page.length === 0) return entries;
|
|
90
|
+
entries.push(...page);
|
|
91
|
+
offset += page.length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createWorkspaceGitFs(workspace: WorkspacePort): FsClient {
|
|
96
|
+
const promises: PromiseFsClient["promises"] = {
|
|
97
|
+
async readFile(
|
|
98
|
+
path: string,
|
|
99
|
+
options?: { encoding?: string } | string,
|
|
100
|
+
): Promise<Uint8Array | string> {
|
|
101
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
102
|
+
try {
|
|
103
|
+
const value = encoding === "utf8" || encoding === "utf-8"
|
|
104
|
+
? await workspace.readFile(path)
|
|
105
|
+
: await workspace.readFileBytes(path);
|
|
106
|
+
if (value === null) throw fsError(path);
|
|
107
|
+
return value;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw fsError(path, error);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async writeFile(path: string, data: string | Uint8Array): Promise<void> {
|
|
113
|
+
const parent = path.replace(/\/[^/]+$/u, "");
|
|
114
|
+
if (parent && parent !== "/" && parent !== path) {
|
|
115
|
+
await workspace.mkdir(parent, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
if (typeof data === "string") await workspace.writeFile(path, data);
|
|
118
|
+
else await workspace.writeFileBytes(path, data);
|
|
119
|
+
},
|
|
120
|
+
async unlink(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await workspace.rm(path);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw fsError(path, error);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async readdir(path: string): Promise<string[]> {
|
|
128
|
+
try {
|
|
129
|
+
const info = await workspace.stat(path);
|
|
130
|
+
if (!info || info.type !== "directory") throw fsError(path);
|
|
131
|
+
return (await readAll(workspace, path)).map(({ name }) => name);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw fsError(path, error);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async mkdir(
|
|
137
|
+
path: string,
|
|
138
|
+
mode?: number | { recursive?: boolean },
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
await workspace.mkdir(path, {
|
|
141
|
+
recursive: typeof mode === "object" ? mode.recursive : false,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
async rmdir(path: string): Promise<void> {
|
|
145
|
+
await workspace.rm(path);
|
|
146
|
+
},
|
|
147
|
+
async stat(path: string): Promise<GitStat> {
|
|
148
|
+
try {
|
|
149
|
+
const info = await workspace.stat(path);
|
|
150
|
+
if (!info) throw fsError(path);
|
|
151
|
+
return new GitStat(info);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw fsError(path, error);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
async lstat(path: string): Promise<GitStat> {
|
|
157
|
+
try {
|
|
158
|
+
const info = await workspace.lstat(path);
|
|
159
|
+
if (!info) throw fsError(path);
|
|
160
|
+
return new GitStat(info);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw fsError(path, error);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async readlink(path: string): Promise<string> {
|
|
166
|
+
try {
|
|
167
|
+
return await workspace.readlink(path);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw fsError(path, error);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
async symlink(target: string, path: string): Promise<void> {
|
|
173
|
+
await workspace.symlink(target, path);
|
|
174
|
+
},
|
|
175
|
+
async chmod(): Promise<void> {},
|
|
176
|
+
};
|
|
177
|
+
return { promises };
|
|
178
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface WorkspaceFileVersion {
|
|
2
|
+
updatedAt: number;
|
|
3
|
+
size: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type WorkspaceConditionalWriteResult =
|
|
7
|
+
| {
|
|
8
|
+
written: true;
|
|
9
|
+
version: WorkspaceFileVersion;
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
written: false;
|
|
13
|
+
reason: "conflict";
|
|
14
|
+
current: WorkspaceFileVersion | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Host-only compare-and-write capability used by Sandbox publishing.
|
|
19
|
+
*
|
|
20
|
+
* Runtime and the model never receive this port. The expected version comes
|
|
21
|
+
* from the automatic hydration snapshot, so a later Workspace edit cannot be
|
|
22
|
+
* overwritten silently.
|
|
23
|
+
*/
|
|
24
|
+
export interface WorkspacePublishPort {
|
|
25
|
+
writeFileBytesIfUnchanged(
|
|
26
|
+
path: string,
|
|
27
|
+
data: Uint8Array,
|
|
28
|
+
mimeType: string,
|
|
29
|
+
expected: WorkspaceFileVersion | null,
|
|
30
|
+
): Promise<WorkspaceConditionalWriteResult>;
|
|
31
|
+
}
|