@opengeni/runtime 2.5.3-canary.0 → 2.5.3-canary.2
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/dist/{chunk-SD6MDONS.js → chunk-3EXLW2IW.js} +150 -34
- package/dist/chunk-3EXLW2IW.js.map +1 -0
- package/dist/{chunk-VVEGJPDR.js → chunk-46BFUGFL.js} +317 -27
- package/dist/chunk-46BFUGFL.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/operational-instructions.d.ts +1 -1
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
- package/dist/sandbox/selfhosted/file-transfer.d.ts +20 -0
- package/dist/sandbox/selfhosted/session.d.ts +7 -0
- package/dist/workspace-skills.d.ts +4 -0
- package/dist/workspace-tool-gateway.js +2 -2
- package/package.json +11 -11
- package/src/bundled_default_skills/document-parsing/SKILL.md +36 -0
- package/src/bundled_site_skills/opengeni-sites/SKILL.md +1 -1
- package/src/lazy-tool-transport.ts +2 -0
- package/src/operational-instructions.ts +1 -1
- package/src/runtime-skills.ts +7 -2
- package/src/sandbox/routing/backend-resolver.ts +2 -0
- package/src/sandbox/selfhosted/control-rpc.ts +22 -0
- package/src/sandbox/selfhosted/fault-rendering.ts +9 -0
- package/src/sandbox/selfhosted/file-transfer.ts +160 -0
- package/src/sandbox/selfhosted/op-testing.ts +3 -0
- package/src/sandbox/selfhosted/session.ts +194 -5
- package/src/workspace-skills.ts +171 -31
- package/dist/chunk-SD6MDONS.js.map +0 -1
- package/dist/chunk-VVEGJPDR.js.map +0 -1
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
type StreamChannel,
|
|
32
32
|
} from "@opengeni/agent-proto";
|
|
33
33
|
import { DESKTOP_STREAM_PORT } from "@opengeni/contracts";
|
|
34
|
+
import {
|
|
35
|
+
SELFHOSTED_FILE_CHUNK_BYTES,
|
|
36
|
+
fileContentDigest,
|
|
37
|
+
transferEditorFile,
|
|
38
|
+
} from "./file-transfer";
|
|
34
39
|
// `Manifest` from the ALLOWED sandbox-leaf entrypoint (`@openai/agents/sandbox`
|
|
35
40
|
// re-exports `@openai/agents-core/sandbox`, which exports the Manifest class) —
|
|
36
41
|
// NOT the agent-loop `@openai/agents` root the sandbox leaf forbids. The live
|
|
@@ -262,6 +267,7 @@ export interface SelfhostedBackgroundCommandAdoption {
|
|
|
262
267
|
/** Exact command-admission snapshot. A worker resolves this immediately before
|
|
263
268
|
* every exec/Git admission; retries and already-started operations retain it. */
|
|
264
269
|
export interface SelfhostedOperationAdmission {
|
|
270
|
+
transactionalFsWriteSupported?: boolean;
|
|
265
271
|
/** Workspace segment of the physical agent/relay route. Personal machines may
|
|
266
272
|
* originate in a different same-organization workspace than the session. */
|
|
267
273
|
workspaceId?: string;
|
|
@@ -284,6 +290,8 @@ export interface SelfhostedOperationAdmission {
|
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
export interface SelfhostedSessionDeps {
|
|
293
|
+
/** Exact live daemon capability; absent preserves legacy small-file writes. */
|
|
294
|
+
transactionalFsWriteSupported?: boolean;
|
|
287
295
|
/** Authorization/session workspace. */
|
|
288
296
|
workspaceId: string;
|
|
289
297
|
/** Physical control-plane workspace. Defaults to workspaceId for legacy and
|
|
@@ -488,6 +496,7 @@ export class SelfhostedSession {
|
|
|
488
496
|
private readonly resourcePolicy: OperationResourcePolicy | undefined;
|
|
489
497
|
private readonly resourcePolicySupported: boolean;
|
|
490
498
|
private readonly operationCpuQuotaSupported: boolean;
|
|
499
|
+
private readonly transactionalFsWriteSupported: boolean;
|
|
491
500
|
private readonly defaultOpStream: SelfhostedOpStreamDeps | undefined;
|
|
492
501
|
private readonly connectionInstanceId: string;
|
|
493
502
|
private readonly resolveOperationAdmission:
|
|
@@ -560,6 +569,7 @@ export class SelfhostedSession {
|
|
|
560
569
|
this.resourcePolicy = normalizeOperationResourcePolicy(deps.operationResourcePolicy);
|
|
561
570
|
this.resourcePolicySupported = deps.operationResourcePolicySupported === true;
|
|
562
571
|
this.operationCpuQuotaSupported = deps.operationCpuQuotaSupported === true;
|
|
572
|
+
this.transactionalFsWriteSupported = deps.transactionalFsWriteSupported === true;
|
|
563
573
|
this.defaultOpStream = deps.opStream;
|
|
564
574
|
this.connectionInstanceId = deps.connectionInstanceId;
|
|
565
575
|
this.resolveOperationAdmission = deps.resolveOperationAdmission;
|
|
@@ -577,6 +587,7 @@ export class SelfhostedSession {
|
|
|
577
587
|
subject: this.subject,
|
|
578
588
|
resourcePolicy: this.resourcePolicy,
|
|
579
589
|
opStream: deps.opStream,
|
|
590
|
+
transactionalFsWriteSupported: this.transactionalFsWriteSupported,
|
|
580
591
|
})
|
|
581
592
|
: undefined;
|
|
582
593
|
// A valid Manifest whose root is the machine's actual effective root. There
|
|
@@ -644,6 +655,7 @@ export class SelfhostedSession {
|
|
|
644
655
|
}
|
|
645
656
|
|
|
646
657
|
private async admitOperation(commandPolicy: boolean): Promise<{
|
|
658
|
+
transactionalFsWriteSupported: boolean;
|
|
647
659
|
controlWorkspaceId: string;
|
|
648
660
|
connectionInstanceId: string;
|
|
649
661
|
subject: string;
|
|
@@ -664,6 +676,7 @@ export class SelfhostedSession {
|
|
|
664
676
|
subject: this.subject,
|
|
665
677
|
resourcePolicy: commandPolicy ? this.resourcePolicy : undefined,
|
|
666
678
|
opStream: this.defaultOpStream,
|
|
679
|
+
transactionalFsWriteSupported: this.transactionalFsWriteSupported,
|
|
667
680
|
};
|
|
668
681
|
}
|
|
669
682
|
|
|
@@ -698,6 +711,7 @@ export class SelfhostedSession {
|
|
|
698
711
|
),
|
|
699
712
|
resourcePolicy: commandPolicy ? resourcePolicy : undefined,
|
|
700
713
|
opStream: resolved.opStream,
|
|
714
|
+
transactionalFsWriteSupported: resolved.transactionalFsWriteSupported === true,
|
|
701
715
|
};
|
|
702
716
|
}
|
|
703
717
|
|
|
@@ -879,7 +893,9 @@ export class SelfhostedSession {
|
|
|
879
893
|
machineId: this.agentId,
|
|
880
894
|
faultClass: selfhostedFaultClass(error),
|
|
881
895
|
// Known only on a PAYLOAD_TOO_LARGE fault (the agent's encoded_bytes detail).
|
|
882
|
-
...(Number.isFinite(encoded)
|
|
896
|
+
...(Number.isFinite(encoded) && error.detail.direction !== "request"
|
|
897
|
+
? { replyBytes: encoded }
|
|
898
|
+
: {}),
|
|
883
899
|
});
|
|
884
900
|
// Fold the retry count into the DRAINING copy when we actually retried, so
|
|
885
901
|
// the surfaced message reads "…retried N times first…".
|
|
@@ -1324,8 +1340,169 @@ export class SelfhostedSession {
|
|
|
1324
1340
|
const pathExists = (path: string): Promise<boolean> => this.pathExists(path, runAs);
|
|
1325
1341
|
const readText = async (path: string): Promise<string> =>
|
|
1326
1342
|
decoder.decode(await this.readFile({ path, ...(runAs ? { runAs } : {}) }));
|
|
1327
|
-
const writeText = async (
|
|
1328
|
-
|
|
1343
|
+
const writeText = async (
|
|
1344
|
+
path: string,
|
|
1345
|
+
content: string,
|
|
1346
|
+
base?: string | (() => Promise<string | undefined>),
|
|
1347
|
+
): Promise<((source: string, sourceBase: string) => Promise<void>) | undefined> => {
|
|
1348
|
+
const bytes = encoder.encode(content);
|
|
1349
|
+
if (bytes.byteLength <= SELFHOSTED_FILE_CHUNK_BYTES) {
|
|
1350
|
+
await this.writeFile({ path, content, createParents: true });
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const initial = await this.admitOperation(false);
|
|
1354
|
+
if (!initial.transactionalFsWriteSupported) {
|
|
1355
|
+
// Older agents may have a larger negotiated message budget. Preserve
|
|
1356
|
+
// their existing writes; a real size rejection remains a typed fault.
|
|
1357
|
+
await this.writeFile({ path, content, createParents: true });
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (runAs) {
|
|
1361
|
+
throw new SelfhostedControlError({
|
|
1362
|
+
code: ErrorCode.ERROR_CODE_UNSUPPORTED,
|
|
1363
|
+
message:
|
|
1364
|
+
"Transactional file editing does not support runAs impersonation; no file was written.",
|
|
1365
|
+
reason: null,
|
|
1366
|
+
retryable: false,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
const requestTransfer = async (
|
|
1370
|
+
requestId: string,
|
|
1371
|
+
op: NonNullable<ControlRequest["op"]>,
|
|
1372
|
+
): Promise<ControlResponse> => {
|
|
1373
|
+
const startedAt = Date.now();
|
|
1374
|
+
try {
|
|
1375
|
+
const current = await this.admitOperation(false);
|
|
1376
|
+
if (
|
|
1377
|
+
!current.transactionalFsWriteSupported ||
|
|
1378
|
+
current.subject !== initial.subject ||
|
|
1379
|
+
current.connectionInstanceId !== initial.connectionInstanceId
|
|
1380
|
+
) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
"The authorized machine connection changed during the file transfer; the operation was not replayed.",
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const response = await this.controlRpc.request(
|
|
1386
|
+
current.subject,
|
|
1387
|
+
{
|
|
1388
|
+
requestId,
|
|
1389
|
+
epoch: this.epoch,
|
|
1390
|
+
op,
|
|
1391
|
+
resourcePolicy: undefined,
|
|
1392
|
+
},
|
|
1393
|
+
{ timeoutMs: this.timeoutMs },
|
|
1394
|
+
);
|
|
1395
|
+
if (response.requestId !== requestId)
|
|
1396
|
+
throw new Error("The machine returned a mismatched file-transfer response identity.");
|
|
1397
|
+
const error = response.error
|
|
1398
|
+
? agentErrorToControlError(response.error, requestId)
|
|
1399
|
+
: undefined;
|
|
1400
|
+
this.emitOp({
|
|
1401
|
+
op: op.$case,
|
|
1402
|
+
outcome: error || !response.result ? "failed" : "ok",
|
|
1403
|
+
healed: false,
|
|
1404
|
+
retries: 0,
|
|
1405
|
+
durationMs: Date.now() - startedAt,
|
|
1406
|
+
machineId: this.agentId,
|
|
1407
|
+
...(error
|
|
1408
|
+
? { code: error.code, reason: error.reason, faultClass: selfhostedFaultClass(error) }
|
|
1409
|
+
: {}),
|
|
1410
|
+
});
|
|
1411
|
+
return response;
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
this.emitOp({
|
|
1414
|
+
op: op.$case,
|
|
1415
|
+
outcome: "failed",
|
|
1416
|
+
healed: false,
|
|
1417
|
+
retries: 0,
|
|
1418
|
+
durationMs: Date.now() - startedAt,
|
|
1419
|
+
machineId: this.agentId,
|
|
1420
|
+
...(error instanceof SelfhostedControlError
|
|
1421
|
+
? { code: error.code, reason: error.reason, faultClass: selfhostedFaultClass(error) }
|
|
1422
|
+
: {}),
|
|
1423
|
+
});
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
const resolvedBase = typeof base === "function" ? await base() : base;
|
|
1428
|
+
const transferStartedAt = Date.now();
|
|
1429
|
+
try {
|
|
1430
|
+
const result = await transferEditorFile({
|
|
1431
|
+
path: resolveConnectedMachinePath(this.workspaceRoot, path),
|
|
1432
|
+
content: bytes,
|
|
1433
|
+
...(resolvedBase === undefined ? {} : { baseContent: encoder.encode(resolvedBase) }),
|
|
1434
|
+
request: requestTransfer,
|
|
1435
|
+
});
|
|
1436
|
+
// Unlike physical request observations, this is a verified file outcome.
|
|
1437
|
+
this.emitOp({
|
|
1438
|
+
op: "fsWrite",
|
|
1439
|
+
outcome: "ok",
|
|
1440
|
+
healed: result.recovered,
|
|
1441
|
+
retries: 0,
|
|
1442
|
+
durationMs: Date.now() - transferStartedAt,
|
|
1443
|
+
machineId: this.agentId,
|
|
1444
|
+
});
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
this.emitOp({
|
|
1447
|
+
op: "fsWrite",
|
|
1448
|
+
outcome: "failed",
|
|
1449
|
+
healed: false,
|
|
1450
|
+
retries: 0,
|
|
1451
|
+
durationMs: Date.now() - transferStartedAt,
|
|
1452
|
+
machineId: this.agentId,
|
|
1453
|
+
});
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
return async (source, sourceBase) => {
|
|
1457
|
+
// A move is verified destination replacement followed by source cleanup,
|
|
1458
|
+
// not an atomic rename. Never retarget cleanup after a runner change.
|
|
1459
|
+
// The source check and deletion are not CAS against unrelated writers.
|
|
1460
|
+
try {
|
|
1461
|
+
const sourcePath = resolveConnectedMachinePath(this.workspaceRoot, source);
|
|
1462
|
+
const sourceBytes: Uint8Array[] = [];
|
|
1463
|
+
let offset = 0;
|
|
1464
|
+
let total = Infinity;
|
|
1465
|
+
while (offset < total) {
|
|
1466
|
+
const reply = await requestTransfer(crypto.randomUUID(), {
|
|
1467
|
+
$case: "fsRead",
|
|
1468
|
+
fsRead: {
|
|
1469
|
+
path: sourcePath,
|
|
1470
|
+
offset: String(offset),
|
|
1471
|
+
length: String(SELFHOSTED_FILE_CHUNK_BYTES),
|
|
1472
|
+
},
|
|
1473
|
+
});
|
|
1474
|
+
if (reply.error) throw agentErrorToControlError(reply.error);
|
|
1475
|
+
if (reply.result?.$case !== "fsRead")
|
|
1476
|
+
throw new Error("Source verification response missing");
|
|
1477
|
+
const part = reply.result.fsRead;
|
|
1478
|
+
total = safeWireSize(part.totalSize, "source file size");
|
|
1479
|
+
if (!part.content.length && offset < total)
|
|
1480
|
+
throw new Error("Source verification was incomplete");
|
|
1481
|
+
sourceBytes.push(part.content);
|
|
1482
|
+
offset += part.content.length;
|
|
1483
|
+
}
|
|
1484
|
+
const actual = new Uint8Array(offset);
|
|
1485
|
+
let cursor = 0;
|
|
1486
|
+
for (const chunk of sourceBytes) {
|
|
1487
|
+
actual.set(chunk, cursor);
|
|
1488
|
+
cursor += chunk.length;
|
|
1489
|
+
}
|
|
1490
|
+
if (fileContentDigest(actual) !== fileContentDigest(encoder.encode(sourceBase)))
|
|
1491
|
+
throw new Error("Source changed before move cleanup");
|
|
1492
|
+
const reply = await requestTransfer(crypto.randomUUID(), {
|
|
1493
|
+
$case: "fsRemove",
|
|
1494
|
+
fsRemove: { path: sourcePath, recursive: false },
|
|
1495
|
+
});
|
|
1496
|
+
if (reply.error) throw agentErrorToControlError(reply.error);
|
|
1497
|
+
if (reply.result?.$case !== "fsRemove")
|
|
1498
|
+
throw new Error("Source cleanup response missing");
|
|
1499
|
+
} catch (cause) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
"The destination edit was verified, but source cleanup was not verified. The move was not replayed; inspect both paths.",
|
|
1502
|
+
{ cause },
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1329
1506
|
};
|
|
1330
1507
|
const deletePath = async (path: string): Promise<void> => {
|
|
1331
1508
|
const result = await this.call({
|
|
@@ -1351,9 +1528,15 @@ export class SelfhostedSession {
|
|
|
1351
1528
|
const current = await readText(operation.path);
|
|
1352
1529
|
const next = applyDiff(current, operation.diff);
|
|
1353
1530
|
const destination = operation.moveTo ?? operation.path;
|
|
1354
|
-
|
|
1531
|
+
const destinationBase =
|
|
1532
|
+
destination === operation.path
|
|
1533
|
+
? current
|
|
1534
|
+
: async () =>
|
|
1535
|
+
(await pathExists(destination)) ? await readText(destination) : undefined;
|
|
1536
|
+
const cleanup = await writeText(destination, next, destinationBase);
|
|
1355
1537
|
if (operation.moveTo && destination !== operation.path) {
|
|
1356
|
-
await
|
|
1538
|
+
if (cleanup) await cleanup(operation.path, current);
|
|
1539
|
+
else await deletePath(operation.path);
|
|
1357
1540
|
}
|
|
1358
1541
|
return {};
|
|
1359
1542
|
},
|
|
@@ -1708,6 +1891,7 @@ export class SelfhostedSandboxClient {
|
|
|
1708
1891
|
private readonly operationResourcePolicy: SelfhostedOperationResourcePolicy | undefined;
|
|
1709
1892
|
private readonly operationResourcePolicySupported: boolean | undefined;
|
|
1710
1893
|
private readonly operationCpuQuotaSupported: boolean | undefined;
|
|
1894
|
+
private readonly transactionalFsWriteSupported: boolean | undefined;
|
|
1711
1895
|
private readonly resolveOperationAdmission:
|
|
1712
1896
|
| (() => Promise<SelfhostedOperationAdmission | null>)
|
|
1713
1897
|
| undefined;
|
|
@@ -1748,6 +1932,7 @@ export class SelfhostedSandboxClient {
|
|
|
1748
1932
|
operationResourcePolicy?: SelfhostedOperationResourcePolicy;
|
|
1749
1933
|
operationResourcePolicySupported?: boolean;
|
|
1750
1934
|
operationCpuQuotaSupported?: boolean;
|
|
1935
|
+
transactionalFsWriteSupported?: boolean;
|
|
1751
1936
|
resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
|
|
1752
1937
|
adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
|
|
1753
1938
|
settleBackgroundCommand?: SelfhostedSessionDeps["settleBackgroundCommand"];
|
|
@@ -1779,6 +1964,7 @@ export class SelfhostedSandboxClient {
|
|
|
1779
1964
|
this.operationResourcePolicy = opts.operationResourcePolicy;
|
|
1780
1965
|
this.operationResourcePolicySupported = opts.operationResourcePolicySupported;
|
|
1781
1966
|
this.operationCpuQuotaSupported = opts.operationCpuQuotaSupported;
|
|
1967
|
+
this.transactionalFsWriteSupported = opts.transactionalFsWriteSupported;
|
|
1782
1968
|
this.resolveOperationAdmission = opts.resolveOperationAdmission;
|
|
1783
1969
|
this.adoptBackgroundCommand = opts.adoptBackgroundCommand;
|
|
1784
1970
|
this.settleBackgroundCommand = opts.settleBackgroundCommand;
|
|
@@ -1822,6 +2008,7 @@ export class SelfhostedSandboxClient {
|
|
|
1822
2008
|
...(this.operationCpuQuotaSupported !== undefined
|
|
1823
2009
|
? { operationCpuQuotaSupported: this.operationCpuQuotaSupported }
|
|
1824
2010
|
: {}),
|
|
2011
|
+
transactionalFsWriteSupported: this.transactionalFsWriteSupported === true,
|
|
1825
2012
|
...(this.resolveOperationAdmission !== undefined
|
|
1826
2013
|
? { resolveOperationAdmission: this.resolveOperationAdmission }
|
|
1827
2014
|
: {}),
|
|
@@ -1941,6 +2128,7 @@ export interface SelfhostedSessionBuild {
|
|
|
1941
2128
|
operationResourcePolicySupported?: boolean;
|
|
1942
2129
|
/** Exact live CPU enforcement capability paired with the initial snapshot. */
|
|
1943
2130
|
operationCpuQuotaSupported?: boolean;
|
|
2131
|
+
transactionalFsWriteSupported?: boolean;
|
|
1944
2132
|
/** Live last-boundary operation admission resolver; see SelfhostedSessionDeps. */
|
|
1945
2133
|
resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
|
|
1946
2134
|
adoptBackgroundCommand?: SelfhostedSessionDeps["adoptBackgroundCommand"];
|
|
@@ -1996,6 +2184,7 @@ export async function buildSelfhostedBackendSession(
|
|
|
1996
2184
|
...(deps.operationCpuQuotaSupported !== undefined
|
|
1997
2185
|
? { operationCpuQuotaSupported: deps.operationCpuQuotaSupported }
|
|
1998
2186
|
: {}),
|
|
2187
|
+
transactionalFsWriteSupported: deps.transactionalFsWriteSupported === true,
|
|
1999
2188
|
...(deps.resolveOperationAdmission !== undefined
|
|
2000
2189
|
? { resolveOperationAdmission: deps.resolveOperationAdmission }
|
|
2001
2190
|
: {}),
|
package/src/workspace-skills.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
409
|
+
return files;
|
|
287
410
|
}
|
|
288
411
|
|
|
289
|
-
function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
436
|
+
await visit("");
|
|
437
|
+
return files;
|
|
298
438
|
}
|
|
299
439
|
|
|
300
440
|
function joinWorkspacePath(parent: string, child: string): string {
|