@opengeni/runtime 2.5.2 → 2.5.3-canary.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.
Files changed (49) hide show
  1. package/dist/{agent-Q2VNH5FV.js → agent-RJQLKFIW.js} +2 -2
  2. package/dist/{chunk-LGLJ5PVD.js → chunk-46BFUGFL.js} +2622 -1368
  3. package/dist/chunk-46BFUGFL.js.map +1 -0
  4. package/dist/{chunk-CD22I76N.js → chunk-FHGLYLBL.js} +2 -2
  5. package/dist/{chunk-25B3EQRW.js → chunk-UNICCRW5.js} +4 -4
  6. package/dist/{chunk-GJC7H2FC.js → chunk-VUEWS4GI.js} +494 -95
  7. package/dist/chunk-VUEWS4GI.js.map +1 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +22 -4
  10. package/dist/{localSources-Z5IJIIRV.js → localSources-PVFCAPN2.js} +3 -3
  11. package/dist/mcp-operation-dispatch.d.ts +45 -0
  12. package/dist/mcp-operation-observation.d.ts +137 -0
  13. package/dist/operational-instructions.d.ts +1 -1
  14. package/dist/sandbox/archive-spool.d.ts +14 -0
  15. package/dist/sandbox/host-archive-spool.d.ts +19 -0
  16. package/dist/sandbox/index.d.ts +5 -1
  17. package/dist/sandbox/index.js +7 -3
  18. package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
  19. package/dist/sandbox/selfhosted/file-transfer.d.ts +20 -0
  20. package/dist/sandbox/selfhosted/session.d.ts +7 -0
  21. package/dist/sandbox/workspace-archive.d.ts +7 -0
  22. package/dist/workspace-skills.d.ts +4 -0
  23. package/dist/workspace-tool-gateway.js +4 -4
  24. package/package.json +11 -11
  25. package/src/bundled_default_skills/document-parsing/SKILL.md +36 -0
  26. package/src/bundled_site_skills/opengeni-sites/SKILL.md +1 -1
  27. package/src/index.ts +231 -45
  28. package/src/lazy-tool-transport.ts +2 -0
  29. package/src/mcp-operation-dispatch.ts +144 -0
  30. package/src/mcp-operation-observation.ts +148 -0
  31. package/src/operational-instructions.ts +1 -1
  32. package/src/runtime-skills.ts +7 -2
  33. package/src/sandbox/archive-spool.ts +16 -0
  34. package/src/sandbox/host-archive-spool.ts +1040 -0
  35. package/src/sandbox/index.ts +292 -203
  36. package/src/sandbox/routing/backend-resolver.ts +2 -0
  37. package/src/sandbox/selfhosted/control-rpc.ts +22 -0
  38. package/src/sandbox/selfhosted/fault-rendering.ts +9 -0
  39. package/src/sandbox/selfhosted/file-transfer.ts +160 -0
  40. package/src/sandbox/selfhosted/op-testing.ts +3 -0
  41. package/src/sandbox/selfhosted/session.ts +194 -5
  42. package/src/sandbox/workspace-archive.ts +54 -1
  43. package/src/workspace-skills.ts +171 -31
  44. package/dist/chunk-GJC7H2FC.js.map +0 -1
  45. package/dist/chunk-LGLJ5PVD.js.map +0 -1
  46. /package/dist/{agent-Q2VNH5FV.js.map → agent-RJQLKFIW.js.map} +0 -0
  47. /package/dist/{chunk-CD22I76N.js.map → chunk-FHGLYLBL.js.map} +0 -0
  48. /package/dist/{chunk-25B3EQRW.js.map → chunk-UNICCRW5.js.map} +0 -0
  49. /package/dist/{localSources-Z5IJIIRV.js.map → localSources-PVFCAPN2.js.map} +0 -0
@@ -1,4 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { tool, type Tool } from "@openai/agents";
3
+ import { parseSkillFrontmatter } from "@opengeni/contracts";
4
+ import { z } from "zod";
5
+ import {
6
+ assertSkillRelativePath,
7
+ listSkillPaths,
8
+ readSkillFiles,
9
+ SkillFileError,
10
+ SKILL_READ_MAX_PATHS,
11
+ SKILL_READ_MAX_OUTPUT_BYTES,
12
+ type SkillTextFile,
13
+ } from "./skill-files";
2
14
 
3
15
  import {
4
16
  Capability,
@@ -64,24 +76,94 @@ export class WorkspaceSkillsCapability extends Capability {
64
76
  super();
65
77
  }
66
78
 
67
- override async instructions(): Promise<string | null> {
79
+ override bind(session: SandboxSessionLike): this {
80
+ if (this._session !== session) delete this.discovery;
81
+ return super.bind(session);
82
+ }
83
+
84
+ private discover(): Promise<readonly WorkspaceSkill[]> {
68
85
  const session = requireWorkspaceSkillSession(this._session);
69
- this.discovery ??= discoverWorkspaceSkills(
86
+ return (this.discovery ??= discoverWorkspaceSkills(
70
87
  session,
71
88
  this.searchPaths,
72
89
  this.reservedNames,
73
90
  this._runAs,
74
91
  this.shadowedNames,
75
- );
76
- const skills = await this.discovery;
92
+ ));
93
+ }
94
+
95
+ override tools(): Tool<unknown>[] {
96
+ return [
97
+ tool({
98
+ name: "repository_skill_read",
99
+ description:
100
+ "Read current repository Skill files from the bound workspace. Use the exact repository: identifier from the Repository skills index. Omit paths for SKILL.md; explicit relative paths read only those files. Set listFiles:true without paths for a bounded file inventory. This reads live files through the sandbox, including Connected Machines.",
101
+ parameters: z.object({
102
+ skill: z.string().min(1),
103
+ paths: z.array(z.string().min(1).max(1024)).min(1).max(SKILL_READ_MAX_PATHS).optional(),
104
+ listFiles: z.boolean().optional(),
105
+ }),
106
+ execute: async (request) => {
107
+ if (request.listFiles && request.paths !== undefined)
108
+ throw new SkillFileError(
109
+ "invalid_request",
110
+ "listFiles:true cannot be combined with paths.",
111
+ );
112
+ const skill = (await this.discover()).find(
113
+ (entry) => repositorySkillId(entry) === request.skill,
114
+ );
115
+ if (!skill)
116
+ throw new SkillFileError(
117
+ "missing_file",
118
+ "Repository Skill is not in this index. Use its exact repository: identifier.",
119
+ );
120
+ const session = requireWorkspaceSkillSession(this._session);
121
+ const root = skill.path.slice(0, -SKILL_FILE.length - 1);
122
+ const result = request.listFiles
123
+ ? listSkillPaths(
124
+ await repositorySkillInventory(session, root, this._runAs),
125
+ MAX_SKILL_ENTRIES,
126
+ )
127
+ : readSkillFiles(
128
+ await repositorySkillFiles(
129
+ session,
130
+ root,
131
+ request.paths ?? [SKILL_FILE],
132
+ this._runAs,
133
+ ),
134
+ request.paths,
135
+ );
136
+ const output = JSON.stringify({
137
+ skillId: request.skill,
138
+ source: "repository",
139
+ root,
140
+ ...result,
141
+ });
142
+ if (new TextEncoder().encode(output).byteLength > SKILL_READ_MAX_OUTPUT_BYTES) {
143
+ throw new SkillFileError(
144
+ "output_too_large",
145
+ "Repository Skill response exceeds the read output limit. Request fewer paths.",
146
+ );
147
+ }
148
+ return output;
149
+ },
150
+ }),
151
+ ];
152
+ }
153
+
154
+ override async instructions(): Promise<string | null> {
155
+ const skills = await this.discover();
77
156
  if (skills.length === 0) return null;
78
157
 
79
158
  const available = skills
80
- .map((skill) => `- ${skill.name}: ${skill.description} (file: ${skill.path})`)
159
+ .map(
160
+ (skill) =>
161
+ `- ${JSON.stringify({ id: repositorySkillId(skill), name: skill.name, description: skill.description, reader: "repository_skill_read", path: skill.path })}`,
162
+ )
81
163
  .join("\n");
82
164
  return `## Repository skills
83
165
 
84
- Repository skills are instructions already present in the live workspace. They do not need to be loaded or copied.
166
+ Repository skills are instructions already present in the live workspace. Read them with repository_skill_read using the exact repository: id below. The reader returns current live files; ordinary filesystem tools execute referenced scripts and read binary assets. Configured Skills use their separate skill_read index.
85
167
 
86
168
  ### Available repository skills
87
169
  ${available}
@@ -165,6 +247,18 @@ async function discoverWorkspaceSkillsUnmeasured(
165
247
  const skillMarkdownPath = joinWorkspacePath(entry.path, SKILL_FILE);
166
248
  let markdown: string;
167
249
  try {
250
+ // Match reader/inventory eligibility: symlink entrypoints are not
251
+ // advertised because the portable filesystem API cannot prove containment.
252
+ const skillEntries = await session.listDir({
253
+ path: entry.path,
254
+ ...(runAs ? { runAs } : {}),
255
+ });
256
+ if (
257
+ !skillEntries.some(
258
+ (candidate) => candidate.name === SKILL_FILE && candidate.type === "file",
259
+ )
260
+ )
261
+ continue;
168
262
  const content = await session.readFile({
169
263
  path: skillMarkdownPath,
170
264
  ...(runAs ? { runAs } : {}),
@@ -182,7 +276,7 @@ async function discoverWorkspaceSkillsUnmeasured(
182
276
  const key = name.toLowerCase();
183
277
  if (shadowedNames.has(key)) continue;
184
278
  const description = frontmatter.description?.trim() || "No description provided.";
185
- if (description.length > 2_048 || /[\r\n]/.test(description)) {
279
+ if (description.length > 2_048) {
186
280
  throw new Error(`Repository skill "${name}" has an invalid description`);
187
281
  }
188
282
  if (reservedNames.has(key)) {
@@ -267,34 +361,80 @@ async function fingerprintWorkspaceDirectory(
267
361
  return hash.digest("hex");
268
362
  }
269
363
 
270
- function parseSkillFrontmatter(markdown: string): {
271
- name?: string;
272
- description?: string;
273
- } {
274
- const lines = markdown.split(/\r?\n/);
275
- if (lines[0]?.trim() !== "---") return {};
276
- const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
277
- if (end === -1) return {};
278
- const values: Record<string, string> = {};
279
- for (const line of lines.slice(1, end)) {
280
- const separator = line.indexOf(":");
281
- if (separator === -1) continue;
282
- const key = line.slice(0, separator).trim();
283
- if (key !== "name" && key !== "description") continue;
284
- values[key] = unquote(line.slice(separator + 1).trim());
364
+ function repositorySkillId(skill: WorkspaceSkill): string {
365
+ return `repository:${skill.path}`;
366
+ }
367
+
368
+ /** Resolve only ordinary directory entries under the discovered Skill root.
369
+ * Never treat a caller identifier as a path or follow a listed symlink. The
370
+ * bound sandbox remains the filesystem authority, including concurrent edits.
371
+ */
372
+ async function repositorySkillFiles(
373
+ session: SandboxSessionLike,
374
+ root: string,
375
+ paths: readonly string[],
376
+ runAs?: string,
377
+ ): Promise<SkillTextFile[]> {
378
+ // Validate the entire request before touching the filesystem.
379
+ readSkillFiles(
380
+ paths.map((path) => ({ path, content: "" })),
381
+ paths,
382
+ );
383
+ const files: SkillTextFile[] = [];
384
+ for (const path of paths) {
385
+ let directory = root;
386
+ const segments = path.split("/");
387
+ for (const [index, name] of segments.entries()) {
388
+ const entries = await session.listDir!({ path: directory, ...(runAs ? { runAs } : {}) });
389
+ const entry = entries.find((candidate) => candidate.name === name);
390
+ const expected = index === segments.length - 1 ? "file" : "dir";
391
+ if (entry?.type !== expected)
392
+ throw new SkillFileError("missing_file", `Repository Skill file is unavailable: ${path}`);
393
+ directory = joinWorkspacePath(directory, name);
394
+ }
395
+ const content = await session.readFile!({ path: directory, ...(runAs ? { runAs } : {}) });
396
+ files.push({
397
+ path,
398
+ content:
399
+ typeof content === "string"
400
+ ? content
401
+ : new TextDecoder("utf-8", { fatal: true }).decode(content),
402
+ });
403
+ // Bound aggregate output as each file arrives; do not truncate accepted text.
404
+ readSkillFiles(
405
+ files,
406
+ files.map((file) => file.path),
407
+ );
285
408
  }
286
- return values;
409
+ return files;
287
410
  }
288
411
 
289
- function unquote(value: string): string {
290
- if (
291
- value.length >= 2 &&
292
- value[0] === value[value.length - 1] &&
293
- (value[0] === '"' || value[0] === "'")
294
- ) {
295
- return value.slice(1, -1);
412
+ async function repositorySkillInventory(
413
+ session: SandboxSessionLike,
414
+ root: string,
415
+ runAs?: string,
416
+ ): Promise<SkillTextFile[]> {
417
+ const files: SkillTextFile[] = [];
418
+ let count = 0;
419
+ async function visit(relative: string): Promise<void> {
420
+ const entries = await session.listDir!({
421
+ path: relative ? joinWorkspacePath(root, relative) : root,
422
+ ...(runAs ? { runAs } : {}),
423
+ });
424
+ for (const entry of entries) {
425
+ if (++count > MAX_SKILL_ENTRIES)
426
+ throw new SkillFileError("output_too_large", "Repository Skill inventory is too large.");
427
+ // Paths derive from validated names, never provider-returned absolute paths.
428
+ assertSkillRelativePath(entry.name);
429
+ if (entry.name.includes("/"))
430
+ throw new SkillFileError("invalid_path", "Invalid repository directory entry.");
431
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
432
+ if (entry.type === "dir") await visit(path);
433
+ else if (entry.type === "file") files.push({ path, content: "" });
434
+ }
296
435
  }
297
- return value;
436
+ await visit("");
437
+ return files;
298
438
  }
299
439
 
300
440
  function joinWorkspacePath(parent: string, child: string): string {