@kontourai/flow-agents 3.7.0 → 3.8.0
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/CHANGELOG.md +12 -0
- package/build/src/builder-flow-runtime.js +102 -7
- package/build/src/cli/assignment-provider.js +13 -1
- package/build/src/cli/workflow-sidecar.js +29 -11
- package/build/src/cli/workflow.js +20 -10
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/docs/getting-started.md +9 -1
- package/docs/spec/builder-flow-runtime.md +36 -2
- package/evals/integration/test_builder_step_producers.sh +37 -4
- package/evals/integration/test_bundle_install.sh +108 -4
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_public_workflow_cli.sh +11 -5
- package/evals/static/test_universal_bundles.sh +44 -1
- package/package.json +1 -1
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-runtime.ts +99 -7
- package/src/cli/assignment-provider.ts +13 -1
- package/src/cli/builder-flow-runtime.test.mjs +250 -18
- package/src/cli/workflow-sidecar.ts +27 -11
- package/src/cli/workflow.ts +20 -10
- package/src/tools/build-universal-bundles.ts +51 -3
|
@@ -915,7 +915,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
915
915
|
if (!check.id) continue;
|
|
916
916
|
const subjectId = `${slug}/${check.id}`;
|
|
917
917
|
const fieldOrBehavior = String(check.summary ?? check.id);
|
|
918
|
-
const
|
|
918
|
+
const gateClaimIdentityVersion = check._gate_claim_identity_version === 2 ? 2 : 1;
|
|
919
|
+
const gateClaimRecordedAt = gateClaimIdentityVersion === 2 && typeof check._gate_claim_recorded_at === "string" ? check._gate_claim_recorded_at : null;
|
|
920
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", gateClaimRecordedAt ? `${fieldOrBehavior}::gate-visit::${gateClaimRecordedAt}` : fieldOrBehavior);
|
|
919
921
|
const evId = `ev:${claimId}`;
|
|
920
922
|
const legacyClaimType = `workflow.check.${check.kind ?? "external"}`;
|
|
921
923
|
|
|
@@ -1071,7 +1073,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1071
1073
|
// restored stamp) takes the currently-active step's id.
|
|
1072
1074
|
const declaredStepId = gateClaimDeclaredStepId ?? (activeStep ? activeStep.stepId : null);
|
|
1073
1075
|
const declaredMetadata: AnyObj = gateClaimExpectationId
|
|
1074
|
-
? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
|
|
1076
|
+
? { ...claimMetadata, gate_claim: { expectation_id: gateClaimExpectationId, claim_type: declared.claimType, subject_type: declared.subjectType, step_id: declaredStepId, ...(gateClaimIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(gateClaimRecordedAt ? { recorded_at: gateClaimRecordedAt } : {}), ...(gateClaimRouteReason ? { route_reason: gateClaimRouteReason } : {}) } }
|
|
1075
1077
|
: claimMetadata;
|
|
1076
1078
|
const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: effectiveStatus, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, ...(declaredMetadata ? { metadata: declaredMetadata } : {}) };
|
|
1077
1079
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [evItem] as Record<string, unknown>[], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
|
|
@@ -1089,7 +1091,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1089
1091
|
if (!criterion.id) continue;
|
|
1090
1092
|
const subjectId = `${slug}/${criterion.id}`;
|
|
1091
1093
|
const fieldOrBehavior = String(criterion.description ?? criterion.id);
|
|
1092
|
-
const
|
|
1094
|
+
const criterionIdentityVersion = criterion.identity_version === 2 ? 2 : 1;
|
|
1095
|
+
const criterionVerifiedAt = criterionIdentityVersion === 2 && typeof criterion.verified_at === "string" ? criterion.verified_at : null;
|
|
1096
|
+
const claimId = generateClaimId(subjectId, "flow-agents.workflow", criterionVerifiedAt ? `${fieldOrBehavior}::verified::${criterionVerifiedAt}` : fieldOrBehavior);
|
|
1093
1097
|
const legacyClaimType = "workflow.acceptance.criterion";
|
|
1094
1098
|
const policy = ensurePolicy(legacyClaimType, "high", []);
|
|
1095
1099
|
const evStatus = criterionStatusToEventStatus(String(criterion.status ?? ""));
|
|
@@ -1105,7 +1109,7 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1105
1109
|
if (declared) {
|
|
1106
1110
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
1107
1111
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
1108
|
-
const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [] }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1112
|
+
const declaredClaimObj: AnyObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [], ...(criterionIdentityVersion === 2 ? { identity_version: 2 } : {}), ...(criterionVerifiedAt ? { verified_at: criterionVerifiedAt } : {}) }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1109
1113
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj as Record<string, unknown>, evidence: [], events: claimEvents as Record<string, unknown>[], policies: [declaredPolicy] as Record<string, unknown>[] });
|
|
1110
1114
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
1111
1115
|
} else {
|
|
@@ -1128,10 +1132,12 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1128
1132
|
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
1129
1133
|
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
1130
1134
|
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
1135
|
+
const critiqueIdentityVersion = c.identity_version === 2 ? 2 : 1;
|
|
1131
1136
|
const critMeta: AnyObj = {
|
|
1132
1137
|
origin: "critique",
|
|
1133
1138
|
reviewer: critiqueReviewer,
|
|
1134
1139
|
reviewed_at: critiqueReviewedAt,
|
|
1140
|
+
...(critiqueIdentityVersion === 2 ? { identity_version: 2 } : {}),
|
|
1135
1141
|
findings: Array.isArray(c.findings) ? c.findings : [],
|
|
1136
1142
|
lanes: Array.isArray(c.lanes) ? c.lanes : [],
|
|
1137
1143
|
review_target: c.review_target && typeof c.review_target === "object" ? c.review_target : { artifacts: [] },
|
|
@@ -1143,7 +1149,9 @@ export async function buildTrustBundle(slug: string, timestamp: string, checks:
|
|
|
1143
1149
|
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
1144
1150
|
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
1145
1151
|
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
1146
|
-
const claimIdSalt =
|
|
1152
|
+
const claimIdSalt = critiqueIdentityVersion === 2
|
|
1153
|
+
? `${fieldOrBehavior}::reviewed::${critiqueReviewedAt}${supersededBy ? `::superseded::${supersededBy}` : ""}`
|
|
1154
|
+
: (supersededBy ? `${fieldOrBehavior}::superseded::${supersededBy}::${critiqueReviewedAt}` : fieldOrBehavior);
|
|
1147
1155
|
const claimId = generateClaimId(subjectId, "flow-agents.workflow", claimIdSalt);
|
|
1148
1156
|
const legacyClaimType = "workflow.critique.review";
|
|
1149
1157
|
const policy = ensurePolicy(legacyClaimType, "medium", []);
|
|
@@ -2727,7 +2735,7 @@ function requireObservedCommandRefs(refs: AnyObj[], observedCommands: ReadonlySe
|
|
|
2727
2735
|
}
|
|
2728
2736
|
}
|
|
2729
2737
|
|
|
2730
|
-
function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string
|
|
2738
|
+
function completePassingCriteria(existing: AnyObj[], raw: string[], observedCommands: ReadonlySet<string>, verifiedAt: string): AnyObj[] {
|
|
2731
2739
|
if (raw.length === 0) die("record-gate-claim requires --criterion-json for a passing tests-evidence claim");
|
|
2732
2740
|
const incoming = raw.map((value) => parseJson(value, "--criterion-json"));
|
|
2733
2741
|
const expectedById = new Map<string, AnyObj>();
|
|
@@ -2746,7 +2754,7 @@ function completePassingCriteria(existing: AnyObj[], raw: string[], observedComm
|
|
|
2746
2754
|
const refs = normalizeEvidenceRefs(criterion.evidence_refs, `criterion ${ids[index]} evidence_refs`);
|
|
2747
2755
|
if (refs.length === 0) die(`criterion ${ids[index]} requires reviewable evidence_refs`);
|
|
2748
2756
|
requireObservedCommandRefs(refs, observedCommands, `criterion ${ids[index]}`);
|
|
2749
|
-
return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs };
|
|
2757
|
+
return { ...expectedById.get(ids[index])!, status: "pass", evidence_refs: refs, identity_version: 2, verified_at: verifiedAt };
|
|
2750
2758
|
});
|
|
2751
2759
|
}
|
|
2752
2760
|
|
|
@@ -3133,6 +3141,8 @@ function checksFromBundle(dir: string): AnyObj[] {
|
|
|
3133
3141
|
if (typeof gc.claim_type === "string") check._gate_claim_declared_type = gc.claim_type;
|
|
3134
3142
|
if (typeof gc.subject_type === "string") check._gate_claim_declared_subject = gc.subject_type;
|
|
3135
3143
|
if (typeof gc.step_id === "string") check._gate_claim_declared_step_id = gc.step_id;
|
|
3144
|
+
if (typeof gc.recorded_at === "string") check._gate_claim_recorded_at = gc.recorded_at;
|
|
3145
|
+
if (gc.identity_version === 2) check._gate_claim_identity_version = 2;
|
|
3136
3146
|
if (typeof gc.route_reason === "string") check._gate_claim_route_reason = gc.route_reason;
|
|
3137
3147
|
};
|
|
3138
3148
|
// #270 CRITICAL/HIGH fix: a claim that is gate-claim-SHAPED but carries NO metadata.gate_claim
|
|
@@ -3294,6 +3304,7 @@ function critiquesFromBundle(dir: string): AnyObj[] {
|
|
|
3294
3304
|
review_target: md.review_target && typeof md.review_target === "object" && !Array.isArray(md.review_target) ? md.review_target : { artifacts: [] },
|
|
3295
3305
|
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
3296
3306
|
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
3307
|
+
...(md.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3297
3308
|
artifact_refs: Array.isArray(md.artifact_refs) ? md.artifact_refs : [],
|
|
3298
3309
|
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
3299
3310
|
};
|
|
@@ -3314,6 +3325,8 @@ function criteriaFromBundle(dir: string): AnyObj[] {
|
|
|
3314
3325
|
description: typeof saved.description === "string" ? saved.description : (claim.fieldOrBehavior || ""),
|
|
3315
3326
|
status: typeof saved.status === "string" ? saved.status : (claim.value ?? "not_verified"),
|
|
3316
3327
|
evidence_refs: Array.isArray(saved.evidence_refs) ? saved.evidence_refs : [],
|
|
3328
|
+
...(typeof saved.verified_at === "string" ? { verified_at: saved.verified_at } : {}),
|
|
3329
|
+
...(saved.identity_version === 2 ? { identity_version: 2 } : {}),
|
|
3317
3330
|
};
|
|
3318
3331
|
})
|
|
3319
3332
|
.filter((criterion: AnyObj) => typeof criterion.id === "string" && criterion.id.length > 0);
|
|
@@ -3369,7 +3382,7 @@ async function recordEvidence(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3369
3382
|
if (!checks.length && opts(p, "surface-trust-json").length === 0) die("record-evidence requires at least one --check-json or --surface-trust-json");
|
|
3370
3383
|
validateAcceptanceEvidenceRefs(dir, p);
|
|
3371
3384
|
// Phase 4c: bundle is the sole verification artifact — stop writing evidence.json and acceptance.json update.
|
|
3372
|
-
const ts = opt(p, "timestamp",
|
|
3385
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3373
3386
|
// #298: readBundleState + merge-by-id instead of unconditionally replacing every prior check —
|
|
3374
3387
|
// record-evidence previously never called checksFromBundle(dir), so it dropped every check
|
|
3375
3388
|
// recorded by an earlier record-evidence/record-check/record-gate-claim call. A later check
|
|
@@ -3576,7 +3589,7 @@ function diagnostic(dir: string, code: string, summary: string): never {
|
|
|
3576
3589
|
async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
3577
3590
|
const dir = artifactDirFrom(p.positional[0] || die("artifact directory is required"));
|
|
3578
3591
|
const slug = taskSlugFor(dir, opt(p, "task-slug"));
|
|
3579
|
-
const ts = opt(p, "timestamp",
|
|
3592
|
+
const ts = opt(p, "timestamp", new Date().toISOString());
|
|
3580
3593
|
const statusVal = opt(p, "status");
|
|
3581
3594
|
if (!["pass", "fail", "not_verified"].includes(statusVal)) die("--status must be one of: pass, fail, not_verified");
|
|
3582
3595
|
const summary = opt(p, "summary") || die("--summary is required");
|
|
@@ -3652,6 +3665,8 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3652
3665
|
status: statusVal,
|
|
3653
3666
|
summary,
|
|
3654
3667
|
_gate_claim_expectation_id: targetExpectation.id,
|
|
3668
|
+
_gate_claim_identity_version: 2,
|
|
3669
|
+
_gate_claim_recorded_at: ts,
|
|
3655
3670
|
...(routeReason ? { _gate_claim_route_reason: routeReason } : {}),
|
|
3656
3671
|
};
|
|
3657
3672
|
|
|
@@ -3690,7 +3705,7 @@ async function recordGateClaim(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3690
3705
|
// SAME expectation id supersedes the earlier check for that expectation (mergeChecksById); a
|
|
3691
3706
|
// gate claim against a different expectation is additive.
|
|
3692
3707
|
const _existingState = readBundleState(dir);
|
|
3693
|
-
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames) : _existingState.criteria;
|
|
3708
|
+
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames, ts) : _existingState.criteria;
|
|
3694
3709
|
if (mustRunTests) {
|
|
3695
3710
|
const liveCritiques = _existingState.critiques.filter((critique) => !critique.superseded_by);
|
|
3696
3711
|
if (liveCritiques.length === 0 || liveCritiques.some((critique) => !critiqueIsCleanAndCurrent(dir, critique))) {
|
|
@@ -3907,7 +3922,8 @@ async function recordCritique(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
3907
3922
|
const critique = {
|
|
3908
3923
|
id: critiqueId,
|
|
3909
3924
|
reviewer: opt(p, "reviewer", "tool-code-reviewer"),
|
|
3910
|
-
reviewed_at: opt(p, "timestamp",
|
|
3925
|
+
reviewed_at: opt(p, "timestamp", new Date().toISOString()),
|
|
3926
|
+
identity_version: 2,
|
|
3911
3927
|
verdict,
|
|
3912
3928
|
summary,
|
|
3913
3929
|
lanes,
|
package/src/cli/workflow.ts
CHANGED
|
@@ -193,7 +193,7 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
|
|
|
193
193
|
// session state cannot change mid-invocation.
|
|
194
194
|
const caller = assertMatchingAssignmentActor(sessionDir, slug);
|
|
195
195
|
const repaired = await recoverBuilderFlowSession({ sessionDir });
|
|
196
|
-
const beforeEvidence =
|
|
196
|
+
const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
|
|
197
197
|
await mainFromPublicWorkflow([
|
|
198
198
|
"record-gate-claim",
|
|
199
199
|
sessionDir,
|
|
@@ -202,26 +202,33 @@ async function evidence(sessionDir: string, argv: string[], json: boolean): Prom
|
|
|
202
202
|
caller.actorKey,
|
|
203
203
|
]);
|
|
204
204
|
|
|
205
|
-
await syncBuilderFlowSession({ sessionDir });
|
|
205
|
+
const synchronized = await syncBuilderFlowSession({ sessionDir });
|
|
206
206
|
|
|
207
207
|
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
208
208
|
const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
|
|
209
|
-
const afterEvidence =
|
|
210
|
-
const
|
|
211
|
-
|
|
209
|
+
const afterEvidence = manifestEvidenceIdentity(run.manifest);
|
|
210
|
+
const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
|
|
211
|
+
const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
|
|
212
|
+
if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
|
|
212
213
|
throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
|
|
213
214
|
}
|
|
215
|
+
if (!synchronized.attached && newEvidence.length !== 0) {
|
|
216
|
+
throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
|
|
217
|
+
}
|
|
214
218
|
const updatedSidecar = readBoundSession(sessionDir).sidecar;
|
|
215
219
|
return immutableReport({
|
|
216
220
|
run_id: run.runId,
|
|
217
221
|
status: run.state.status,
|
|
218
222
|
current_step: run.state.current_step,
|
|
219
|
-
attached:
|
|
223
|
+
attached: synchronized.attached,
|
|
224
|
+
awaiting_evidence: !synchronized.attached,
|
|
220
225
|
next_action: updatedSidecar.next_action ?? null,
|
|
221
226
|
});
|
|
222
227
|
});
|
|
223
228
|
if (json) console.log(JSON.stringify(report));
|
|
224
|
-
else console.log(
|
|
229
|
+
else console.log(report.attached
|
|
230
|
+
? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
|
|
231
|
+
: `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
|
|
225
232
|
return 0;
|
|
226
233
|
}
|
|
227
234
|
|
|
@@ -511,11 +518,14 @@ function immutableReport<T>(value: T): T {
|
|
|
511
518
|
return Object.freeze(value);
|
|
512
519
|
}
|
|
513
520
|
|
|
514
|
-
function
|
|
521
|
+
function manifestEvidenceIdentity(manifest: JsonRecord): Array<{ id: string; sha256: string }> {
|
|
515
522
|
const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
|
|
516
523
|
return evidence
|
|
517
|
-
.
|
|
518
|
-
|
|
524
|
+
.flatMap((entry) => entry && typeof entry === "object"
|
|
525
|
+
&& typeof (entry as JsonRecord).id === "string"
|
|
526
|
+
&& typeof (entry as JsonRecord).sha256 === "string"
|
|
527
|
+
? [{ id: String((entry as JsonRecord).id), sha256: String((entry as JsonRecord).sha256) }]
|
|
528
|
+
: []);
|
|
519
529
|
}
|
|
520
530
|
|
|
521
531
|
function optionalFileDigest(file: string): string | null {
|
|
@@ -133,6 +133,54 @@ function copyTree(src: string, dest: string, target: string, rootReplacement: st
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
const skillResourcePattern = /(?:^|[`("'\s])(context\/contracts\/[A-Za-z0-9._/-]+\.md)(?=$|[`),"'\s])/gm;
|
|
137
|
+
|
|
138
|
+
/** Export a skill as a self-contained package. Shared instruction resources
|
|
139
|
+
* retain the relative paths used by SKILL.md so runtime resolution is local. */
|
|
140
|
+
function exportSkillPackage(src: string, dest: string, target: string): void {
|
|
141
|
+
const sourceDir = path.dirname(src);
|
|
142
|
+
copyTree(sourceDir, dest, target, "<bundle-root>");
|
|
143
|
+
const skillText = readText(src);
|
|
144
|
+
const resources = new Set<string>();
|
|
145
|
+
for (const match of skillText.matchAll(skillResourcePattern)) resources.add(match[1]);
|
|
146
|
+
for (const rel of [...resources].sort()) {
|
|
147
|
+
if (path.isAbsolute(rel) || rel.split("/").includes("..")) {
|
|
148
|
+
throw new Error(`skill '${path.basename(sourceDir)}': unsafe local resource '${rel}'`);
|
|
149
|
+
}
|
|
150
|
+
const source = path.resolve(root, rel);
|
|
151
|
+
const output = path.resolve(dest, rel);
|
|
152
|
+
if (!source.startsWith(`${path.resolve(root)}${path.sep}`) || !output.startsWith(`${path.resolve(dest)}${path.sep}`)) {
|
|
153
|
+
throw new Error(`skill '${path.basename(sourceDir)}': local resource escapes package '${rel}'`);
|
|
154
|
+
}
|
|
155
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
156
|
+
throw new Error(`skill '${path.basename(sourceDir)}': missing local resource '${rel}'`);
|
|
157
|
+
}
|
|
158
|
+
if (fs.existsSync(output)) {
|
|
159
|
+
const existing = fs.readFileSync(output);
|
|
160
|
+
const incoming = fs.readFileSync(source);
|
|
161
|
+
if (!existing.equals(incoming)) throw new Error(`skill '${path.basename(sourceDir)}': resource collision '${rel}'`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
writeText(output, sanitizeText(readText(source), target, "<bundle-root>"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateExportedSkillPackages(skillsRoot: string): void {
|
|
169
|
+
if (!fs.existsSync(skillsRoot)) return;
|
|
170
|
+
for (const skillName of fs.readdirSync(skillsRoot).sort()) {
|
|
171
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
172
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
173
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
174
|
+
for (const match of readText(skillFile).matchAll(skillResourcePattern)) {
|
|
175
|
+
const rel = match[1];
|
|
176
|
+
const resolved = path.resolve(skillDir, rel);
|
|
177
|
+
if (!resolved.startsWith(`${path.resolve(skillDir)}${path.sep}`) || !fs.existsSync(resolved)) {
|
|
178
|
+
throw new Error(`skill '${skillName}': unresolved exported resource '${rel}'`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
136
184
|
function resolveSourcePath(pathText: string): string {
|
|
137
185
|
let normalized = pathText;
|
|
138
186
|
for (const alias of manifest.source_root_aliases) normalized = normalized.split(alias).join(root);
|
|
@@ -440,9 +488,9 @@ function buildCodex(agents: Agent[]): void {
|
|
|
440
488
|
for (const [profileName, profile] of Object.entries(manifest.codex.profiles ?? {})) writeText(path.join(bundle, ".codex", `${profileName}.config.toml`), exportCodexProfileConfig(profile as Record<string, unknown>, settings));
|
|
441
489
|
writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
|
|
442
490
|
for (const spec of targetAgents) writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
491
|
+
const skillsRoot = path.join(bundle, ".agents/skills");
|
|
492
|
+
for (const { name, src } of collectAllSkills()) exportSkillPackage(src, path.join(skillsRoot, name), "codex");
|
|
493
|
+
validateExportedSkillPackages(skillsRoot);
|
|
446
494
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
|
|
447
495
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace", CODEX_LIVE_HOOKS_README));
|
|
448
496
|
writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace", undefined, undefined, { configRelPath: ".codex/hooks.json", managedConfigRelPath: ".codex/hooks.json", runtime: "codex", version: pkgVersion }));
|