@llblab/pi-actors 0.35.0 → 0.37.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/AGENTS.md +3 -1
- package/BACKLOG.md +4 -72
- package/CHANGELOG.md +17 -3
- package/README.md +2 -1
- package/dist/index.js +0 -1
- package/dist/lib/async-runs.js +4 -3
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +95 -5
- package/dist/lib/recipes-discovery.d.ts +2 -0
- package/dist/lib/recipes-discovery.js +52 -5
- package/dist/lib/recipes-references.d.ts +1 -0
- package/dist/lib/recipes-references.js +122 -35
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +6 -6
- package/dist/lib/runtime-notifier.js +8 -3
- package/dist/lib/runtime.d.ts +2 -6
- package/dist/lib/runtime.js +10 -11
- package/dist/lib/tools-inspect.js +172 -4
- package/dist/lib/tools-response.js +13 -2
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/scripts/validate-recipe.mjs +164 -7
- package/dist/skills/actors/SKILL.md +45 -13
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +20 -1
- package/docs/tool-registry.md +15 -10
- package/index.ts +0 -1
- package/lib/async-runs.ts +4 -7
- package/lib/command-templates.ts +146 -7
- package/lib/recipes-discovery.ts +69 -6
- package/lib/recipes-references.ts +201 -34
- package/lib/registry.ts +5 -5
- package/lib/runtime-notifier.ts +9 -3
- package/lib/runtime.ts +10 -16
- package/lib/tools-inspect.ts +186 -4
- package/lib/tools-response.ts +16 -2
- package/lib/tools.ts +2 -2
- package/package.json +3 -2
- package/scripts/validate-recipe.mjs +164 -7
- package/skills/actors/SKILL.md +45 -13
- package/skills/swarm/SKILL.md +1 -1
package/lib/recipes-discovery.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface DiscoveredRecipe {
|
|
|
27
27
|
tool: boolean;
|
|
28
28
|
mutableUsage: boolean;
|
|
29
29
|
diagnostics: string[];
|
|
30
|
+
riskLabels: CommandTemplates.CommandTemplateRiskLabel[];
|
|
30
31
|
shadows: string[];
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -110,6 +111,14 @@ function listRecipeFiles(root: string): string[] {
|
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
function getRecipeCommandTemplateConfig(
|
|
115
|
+
config: TemplateRecipeConfig,
|
|
116
|
+
): CommandTemplates.CommandTemplateConfig {
|
|
117
|
+
return (typeof config.template === "object" && config.template !== null
|
|
118
|
+
? config.template
|
|
119
|
+
: config) as CommandTemplates.CommandTemplateConfig;
|
|
120
|
+
}
|
|
121
|
+
|
|
113
122
|
function getRecipeConfigDiagnostics(
|
|
114
123
|
file: string,
|
|
115
124
|
config: TemplateRecipeConfig | undefined,
|
|
@@ -118,15 +127,36 @@ function getRecipeConfigDiagnostics(
|
|
|
118
127
|
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(file);
|
|
119
128
|
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
120
129
|
}
|
|
121
|
-
const commandTemplateConfig =
|
|
122
|
-
typeof config.template === "object" && config.template !== null
|
|
123
|
-
? config.template
|
|
124
|
-
: config;
|
|
125
130
|
return CommandTemplates.getCommandTemplateWarnings(
|
|
126
|
-
|
|
131
|
+
getRecipeCommandTemplateConfig(config),
|
|
127
132
|
).map((warning) => `Recipe ${file}: ${warning}`);
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
function getRecipeRiskLabels(
|
|
136
|
+
config: TemplateRecipeConfig | undefined,
|
|
137
|
+
): CommandTemplates.CommandTemplateRiskLabel[] {
|
|
138
|
+
if (!config) return [];
|
|
139
|
+
const labels = new Set(
|
|
140
|
+
CommandTemplates.getCommandTemplateRiskLabels(
|
|
141
|
+
getRecipeCommandTemplateConfig(config),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
if (config.async === true) labels.add("risk.long_running");
|
|
145
|
+
return [
|
|
146
|
+
"risk.shell",
|
|
147
|
+
"risk.eval",
|
|
148
|
+
"risk.destructive_fs",
|
|
149
|
+
"risk.broad_fs_write",
|
|
150
|
+
"risk.external_side_effect",
|
|
151
|
+
"risk.secret_touching",
|
|
152
|
+
"risk.network",
|
|
153
|
+
"risk.long_running",
|
|
154
|
+
"risk.platform_specific",
|
|
155
|
+
].filter((label): label is CommandTemplates.CommandTemplateRiskLabel =>
|
|
156
|
+
labels.has(label as CommandTemplates.CommandTemplateRiskLabel),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
130
160
|
function readDiscoveredRecipe(
|
|
131
161
|
root: string,
|
|
132
162
|
file: string,
|
|
@@ -152,6 +182,7 @@ function readDiscoveredRecipe(
|
|
|
152
182
|
tool: defaultTool && !disabled && !invalid,
|
|
153
183
|
mutableUsage,
|
|
154
184
|
diagnostics: getRecipeConfigDiagnostics(file, config),
|
|
185
|
+
riskLabels: getRecipeRiskLabels(config),
|
|
155
186
|
shadows: [],
|
|
156
187
|
};
|
|
157
188
|
} catch (error) {
|
|
@@ -169,6 +200,7 @@ function readDiscoveredRecipe(
|
|
|
169
200
|
diagnostics: [
|
|
170
201
|
`Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
202
|
],
|
|
203
|
+
riskLabels: [],
|
|
172
204
|
shadows: [],
|
|
173
205
|
};
|
|
174
206
|
}
|
|
@@ -392,7 +424,7 @@ function diagnosticSeverity(message: string): "info" | "warning" | "error" {
|
|
|
392
424
|
return "error";
|
|
393
425
|
}
|
|
394
426
|
if (
|
|
395
|
-
/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(
|
|
427
|
+
/world-writable|group-writable|invokes bash|eval|destructive|broad filesystem|unsafe/i.test(
|
|
396
428
|
message,
|
|
397
429
|
)
|
|
398
430
|
) {
|
|
@@ -435,6 +467,7 @@ function diagnosticDetails(
|
|
|
435
467
|
seen.add(key);
|
|
436
468
|
details.push({
|
|
437
469
|
...(entry ? { id: entry.id, path: entry.path } : {}),
|
|
470
|
+
...(entry?.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
438
471
|
action: diagnosticSuggestedAction(message),
|
|
439
472
|
message,
|
|
440
473
|
severity: diagnosticSeverity(message),
|
|
@@ -496,6 +529,7 @@ function remediationForEntry(
|
|
|
496
529
|
kind: "risky_shell_boundary",
|
|
497
530
|
severity: "warning",
|
|
498
531
|
path: entry.path,
|
|
532
|
+
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
499
533
|
reason: riskyDiagnostics[0],
|
|
500
534
|
action:
|
|
501
535
|
"audit trusted command boundary; keep only if the recipe is local and intentional",
|
|
@@ -544,6 +578,31 @@ function discoveryRemediations(
|
|
|
544
578
|
);
|
|
545
579
|
}
|
|
546
580
|
|
|
581
|
+
function summarizeRiskLabels(
|
|
582
|
+
entries: DiscoveredRecipe[],
|
|
583
|
+
): Array<Record<string, unknown>> {
|
|
584
|
+
const counts = new Map<string, { count: number; ids: Set<string> }>();
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
for (const label of entry.riskLabels) {
|
|
587
|
+
const current = counts.get(label) ?? { count: 0, ids: new Set<string>() };
|
|
588
|
+
current.count += 1;
|
|
589
|
+
current.ids.add(entry.id);
|
|
590
|
+
counts.set(label, current);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return [...counts.entries()]
|
|
594
|
+
.map(([label, value]) => ({
|
|
595
|
+
label,
|
|
596
|
+
count: value.count,
|
|
597
|
+
recipes: [...value.ids].sort(),
|
|
598
|
+
}))
|
|
599
|
+
.sort(
|
|
600
|
+
(a, b) =>
|
|
601
|
+
Number(b.count) - Number(a.count) ||
|
|
602
|
+
String(a.label).localeCompare(String(b.label)),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
547
606
|
function recommendationForEntry(
|
|
548
607
|
entry: DiscoveredRecipe,
|
|
549
608
|
activePath: string | undefined,
|
|
@@ -598,6 +657,7 @@ export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
|
|
|
598
657
|
/spawn run ([^\s]+)/,
|
|
599
658
|
)?.[1];
|
|
600
659
|
const preview = templatePreview(config?.template);
|
|
660
|
+
const riskLabels = getRecipeRiskLabels(resolved);
|
|
601
661
|
return {
|
|
602
662
|
id,
|
|
603
663
|
path,
|
|
@@ -607,6 +667,7 @@ export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
|
|
|
607
667
|
modified_at: stat.mtime.toISOString(),
|
|
608
668
|
valid: Boolean(resolved),
|
|
609
669
|
diagnostics,
|
|
670
|
+
...(riskLabels.length ? { risk_labels: riskLabels } : {}),
|
|
610
671
|
...(config?.description ? { description: config.description } : {}),
|
|
611
672
|
...(sourceRun ? { source_run: sourceRun } : {}),
|
|
612
673
|
...(config?.async !== undefined ? { async: config.async } : {}),
|
|
@@ -639,6 +700,7 @@ export function summarizeDiscovery(
|
|
|
639
700
|
disabled: entry.disabled,
|
|
640
701
|
invalid: entry.invalid,
|
|
641
702
|
shadows: entry.shadows,
|
|
703
|
+
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
642
704
|
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
643
705
|
...(recipeUsage(entry.config)
|
|
644
706
|
? { usage: recipeUsage(entry.config) }
|
|
@@ -665,6 +727,7 @@ export function summarizeDiscovery(
|
|
|
665
727
|
.filter((entry) => entry.disabled)
|
|
666
728
|
.map((entry) => ({ id: entry.id, path: entry.path }))
|
|
667
729
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
730
|
+
risk_summary: summarizeRiskLabels(result.entries),
|
|
668
731
|
recommendations,
|
|
669
732
|
remediations,
|
|
670
733
|
top_action: remediations[0],
|
|
@@ -135,27 +135,55 @@ function recipeNameFiles(value: string): string[] {
|
|
|
135
135
|
return [`${trimmed}.json`, `${trimmed}.md`];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function
|
|
138
|
+
function recipeCandidatePaths(
|
|
139
139
|
value: string,
|
|
140
140
|
currentRecipeRoot: string,
|
|
141
|
-
): string {
|
|
141
|
+
): string[] {
|
|
142
142
|
if (!isBareRecipeName(value))
|
|
143
|
-
return resolveRecipePath(value, currentRecipeRoot);
|
|
143
|
+
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
144
144
|
const roots = [
|
|
145
145
|
Paths.getRecipeRoot(),
|
|
146
146
|
currentRecipeRoot,
|
|
147
147
|
Paths.getPackagedRecipeRoot(),
|
|
148
148
|
];
|
|
149
|
-
|
|
149
|
+
return [
|
|
150
150
|
...new Set(
|
|
151
151
|
roots.flatMap((root) =>
|
|
152
152
|
recipeNameFiles(value).map((file) => resolve(root, file)),
|
|
153
153
|
),
|
|
154
154
|
),
|
|
155
155
|
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRecipeImportPath(
|
|
159
|
+
value: string,
|
|
160
|
+
currentRecipeRoot: string,
|
|
161
|
+
): string {
|
|
162
|
+
const candidates = recipeCandidatePaths(value, currentRecipeRoot);
|
|
156
163
|
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
export function resolveRecipeReferencePath(
|
|
167
|
+
value: unknown,
|
|
168
|
+
currentRecipeRoot = Paths.getRecipeRoot(),
|
|
169
|
+
): string | undefined {
|
|
170
|
+
if (typeof value !== "string") return undefined;
|
|
171
|
+
const trimmed = value.trim();
|
|
172
|
+
if (!trimmed || hasWhitespace(trimmed)) return undefined;
|
|
173
|
+
for (const path of recipeCandidatePaths(trimmed, currentRecipeRoot)) {
|
|
174
|
+
if (!existsSync(path)) continue;
|
|
175
|
+
try {
|
|
176
|
+
const raw = readRawRecipeConfig(path);
|
|
177
|
+
if (raw && typeof raw === "object" && Object.hasOwn(raw, "template"))
|
|
178
|
+
return path;
|
|
179
|
+
return path;
|
|
180
|
+
} catch {
|
|
181
|
+
return path;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
export function getRecipePath(
|
|
160
188
|
value: unknown,
|
|
161
189
|
recipeRoot = Paths.getRecipeRoot(),
|
|
@@ -165,18 +193,7 @@ export function getRecipePath(
|
|
|
165
193
|
if (!trimmed || hasWhitespace(trimmed)) return undefined;
|
|
166
194
|
if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
|
|
167
195
|
return resolveRecipePath(trimmed, recipeRoot);
|
|
168
|
-
|
|
169
|
-
const mdPath = resolveRecipePath(`${trimmed}.md`, recipeRoot);
|
|
170
|
-
const path = existsSync(jsonPath) ? jsonPath : mdPath;
|
|
171
|
-
if (!existsSync(path)) return undefined;
|
|
172
|
-
try {
|
|
173
|
-
const raw = readRawRecipeConfig(path);
|
|
174
|
-
return raw && typeof raw === "object" && Object.hasOwn(raw, "template")
|
|
175
|
-
? path
|
|
176
|
-
: undefined;
|
|
177
|
-
} catch {
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
196
|
+
return resolveRecipeReferencePath(trimmed, recipeRoot);
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
function isImportNode(value: unknown): boolean {
|
|
@@ -483,7 +500,7 @@ export function getRecipeIdFromPath(file: string): string {
|
|
|
483
500
|
}
|
|
484
501
|
|
|
485
502
|
function readRecipeConfig(value: unknown): TemplateRecipeConfig | undefined {
|
|
486
|
-
const path =
|
|
503
|
+
const path = resolveRecipeReferencePath(value);
|
|
487
504
|
return path ? readResolvedRecipeConfig(path) : undefined;
|
|
488
505
|
}
|
|
489
506
|
|
|
@@ -647,6 +664,7 @@ function applyDefaultsToTemplate(
|
|
|
647
664
|
): CommandTemplateValue {
|
|
648
665
|
const cleanOverrides = { ...overrides };
|
|
649
666
|
delete cleanOverrides.name;
|
|
667
|
+
delete cleanOverrides.template;
|
|
650
668
|
delete cleanOverrides.values;
|
|
651
669
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
652
670
|
return {
|
|
@@ -686,6 +704,119 @@ function withActorRecipeContext(
|
|
|
686
704
|
return { actorRecipeContext: context, template: value };
|
|
687
705
|
}
|
|
688
706
|
|
|
707
|
+
function loadDelegatedRecipe(
|
|
708
|
+
value: unknown,
|
|
709
|
+
currentRecipeFile: string,
|
|
710
|
+
stack: string[],
|
|
711
|
+
options: ReadResolvedRecipeConfigOptions,
|
|
712
|
+
): TemplateRecipeConfig | undefined {
|
|
713
|
+
const path = resolveRecipeReferencePath(value, dirname(currentRecipeFile));
|
|
714
|
+
if (!path) return undefined;
|
|
715
|
+
const config = readResolvedRecipeConfig(
|
|
716
|
+
path,
|
|
717
|
+
[...stack, currentRecipeFile],
|
|
718
|
+
options,
|
|
719
|
+
);
|
|
720
|
+
if (!config) throw new Error(`Template recipe must define template: ${path}`);
|
|
721
|
+
if (config.disabled === true)
|
|
722
|
+
throw new Error(`Template recipe is disabled: ${path}`);
|
|
723
|
+
return config;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function applyDelegatedRecipeToNode(
|
|
727
|
+
delegated: TemplateRecipeConfig,
|
|
728
|
+
overrides: Record<string, unknown> = {},
|
|
729
|
+
): CommandTemplateValue {
|
|
730
|
+
return applyDefaultsToTemplate(
|
|
731
|
+
delegated.template,
|
|
732
|
+
delegated.values,
|
|
733
|
+
overrides,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function expandRecipeDelegations(
|
|
738
|
+
value: CommandTemplateValue,
|
|
739
|
+
currentRecipeFile: string,
|
|
740
|
+
stack: string[],
|
|
741
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
742
|
+
): CommandTemplateValue {
|
|
743
|
+
if (typeof value === "string") {
|
|
744
|
+
const delegated = loadDelegatedRecipe(
|
|
745
|
+
value,
|
|
746
|
+
currentRecipeFile,
|
|
747
|
+
stack,
|
|
748
|
+
options,
|
|
749
|
+
);
|
|
750
|
+
return delegated ? applyDelegatedRecipeToNode(delegated) : value;
|
|
751
|
+
}
|
|
752
|
+
if (Array.isArray(value)) {
|
|
753
|
+
return value.map(
|
|
754
|
+
(item) =>
|
|
755
|
+
expandRecipeDelegations(
|
|
756
|
+
item as CommandTemplateValue,
|
|
757
|
+
currentRecipeFile,
|
|
758
|
+
stack,
|
|
759
|
+
options,
|
|
760
|
+
) as CommandTemplateConfig,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
const record = value as Record<string, unknown>;
|
|
764
|
+
if (typeof record.template === "string") {
|
|
765
|
+
const delegated = loadDelegatedRecipe(
|
|
766
|
+
record.template,
|
|
767
|
+
currentRecipeFile,
|
|
768
|
+
stack,
|
|
769
|
+
options,
|
|
770
|
+
);
|
|
771
|
+
if (delegated) return applyDelegatedRecipeToNode(delegated, record);
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(record.template)) {
|
|
774
|
+
return {
|
|
775
|
+
...record,
|
|
776
|
+
template: record.template.map(
|
|
777
|
+
(item) =>
|
|
778
|
+
expandRecipeDelegations(
|
|
779
|
+
item as CommandTemplateValue,
|
|
780
|
+
currentRecipeFile,
|
|
781
|
+
stack,
|
|
782
|
+
options,
|
|
783
|
+
) as CommandTemplateConfig,
|
|
784
|
+
),
|
|
785
|
+
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
786
|
+
}
|
|
787
|
+
if (record.template && typeof record.template === "object") {
|
|
788
|
+
return {
|
|
789
|
+
...record,
|
|
790
|
+
template: expandRecipeDelegations(
|
|
791
|
+
record.template as CommandTemplateValue,
|
|
792
|
+
currentRecipeFile,
|
|
793
|
+
stack,
|
|
794
|
+
options,
|
|
795
|
+
),
|
|
796
|
+
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
797
|
+
}
|
|
798
|
+
return value;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function getDirectDelegatedRecipe(
|
|
802
|
+
value: CommandTemplateValue,
|
|
803
|
+
currentRecipeFile: string,
|
|
804
|
+
stack: string[],
|
|
805
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
806
|
+
): TemplateRecipeConfig | undefined {
|
|
807
|
+
if (typeof value === "string")
|
|
808
|
+
return loadDelegatedRecipe(value, currentRecipeFile, stack, options);
|
|
809
|
+
if (!Array.isArray(value) && typeof value.template === "string") {
|
|
810
|
+
return loadDelegatedRecipe(
|
|
811
|
+
value.template,
|
|
812
|
+
currentRecipeFile,
|
|
813
|
+
stack,
|
|
814
|
+
options,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
|
|
689
820
|
function expandImportNodes(
|
|
690
821
|
value: CommandTemplateValue,
|
|
691
822
|
imports: Record<string, ImportedRecipe>,
|
|
@@ -807,7 +938,22 @@ export function readResolvedRecipeConfig(
|
|
|
807
938
|
>;
|
|
808
939
|
const template = getRecipeCommandTemplate(substituted);
|
|
809
940
|
if (!template) return undefined;
|
|
810
|
-
const
|
|
941
|
+
const expandedImportsTemplate = expandImportNodes(template, imports, options);
|
|
942
|
+
const delegated = getDirectDelegatedRecipe(
|
|
943
|
+
expandedImportsTemplate,
|
|
944
|
+
path,
|
|
945
|
+
stack,
|
|
946
|
+
options,
|
|
947
|
+
);
|
|
948
|
+
const expandedTemplate = delegated
|
|
949
|
+
? applyDelegatedRecipeToNode(
|
|
950
|
+
delegated,
|
|
951
|
+
typeof expandedImportsTemplate === "object" &&
|
|
952
|
+
!Array.isArray(expandedImportsTemplate)
|
|
953
|
+
? (expandedImportsTemplate as Record<string, unknown>)
|
|
954
|
+
: {},
|
|
955
|
+
)
|
|
956
|
+
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
811
957
|
const recipeName = getRecipeIdFromPath(path);
|
|
812
958
|
const templateWithContext = options.includeActorRecipeContext
|
|
813
959
|
? withActorRecipeContext(expandedTemplate, {
|
|
@@ -817,33 +963,53 @@ export function readResolvedRecipeConfig(
|
|
|
817
963
|
role: stack.length > 0 ? "import" : "entry",
|
|
818
964
|
})
|
|
819
965
|
: expandedTemplate;
|
|
966
|
+
const mergedDefaults = mergeDefaults(
|
|
967
|
+
delegated?.defaults,
|
|
968
|
+
isRecord(substituted.defaults) ? substituted.defaults : undefined,
|
|
969
|
+
);
|
|
970
|
+
const artifactSource = isRecord(substituted.artifacts)
|
|
971
|
+
? substituted.artifacts
|
|
972
|
+
: delegated?.artifacts;
|
|
973
|
+
const mailboxSource = isRecord(substituted.mailbox)
|
|
974
|
+
? substituted.mailbox
|
|
975
|
+
: delegated?.mailbox;
|
|
820
976
|
return {
|
|
821
977
|
name: recipeName,
|
|
822
978
|
...(typeof substituted.description === "string" &&
|
|
823
979
|
substituted.description.trim()
|
|
824
980
|
? { description: substituted.description.trim() }
|
|
825
|
-
:
|
|
981
|
+
: typeof delegated?.description === "string"
|
|
982
|
+
? { description: delegated.description }
|
|
983
|
+
: {}),
|
|
826
984
|
...(typeof substituted.disabled === "boolean"
|
|
827
985
|
? { disabled: substituted.disabled }
|
|
828
|
-
:
|
|
986
|
+
: typeof delegated?.disabled === "boolean"
|
|
987
|
+
? { disabled: delegated.disabled }
|
|
988
|
+
: {}),
|
|
829
989
|
...(substituted.async === true
|
|
830
990
|
? { async: true }
|
|
831
991
|
: substituted.async === false
|
|
832
992
|
? { async: false }
|
|
833
|
-
:
|
|
993
|
+
: delegated?.async === true
|
|
994
|
+
? { async: true }
|
|
995
|
+
: delegated?.async === false
|
|
996
|
+
? { async: false }
|
|
997
|
+
: {}),
|
|
834
998
|
...(typeof substituted.state_dir === "string"
|
|
835
999
|
? { state_dir: substituted.state_dir }
|
|
836
|
-
:
|
|
1000
|
+
: typeof delegated?.state_dir === "string"
|
|
1001
|
+
? { state_dir: delegated.state_dir }
|
|
1002
|
+
: {}),
|
|
837
1003
|
...(Object.keys(imports).length > 0
|
|
838
1004
|
? { imports: getRecipeImports(raw) }
|
|
839
1005
|
: {}),
|
|
840
1006
|
template: templateWithContext,
|
|
841
1007
|
...(Array.isArray(substituted.args)
|
|
842
1008
|
? { args: substituted.args as string[] }
|
|
843
|
-
:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1009
|
+
: Array.isArray(delegated?.args)
|
|
1010
|
+
? { args: delegated.args }
|
|
1011
|
+
: {}),
|
|
1012
|
+
...(mergedDefaults ? { defaults: mergedDefaults } : {}),
|
|
847
1013
|
...(typeof substituted.parallel === "boolean"
|
|
848
1014
|
? { parallel: substituted.parallel }
|
|
849
1015
|
: {}),
|
|
@@ -865,31 +1031,31 @@ export function readResolvedRecipeConfig(
|
|
|
865
1031
|
...(typeof substituted.output === "string"
|
|
866
1032
|
? { output: substituted.output }
|
|
867
1033
|
: {}),
|
|
868
|
-
...(isRecord(
|
|
1034
|
+
...(isRecord(artifactSource)
|
|
869
1035
|
? {
|
|
870
1036
|
artifacts: Object.fromEntries(
|
|
871
|
-
Object.entries(
|
|
1037
|
+
Object.entries(artifactSource).filter(
|
|
872
1038
|
(entry): entry is [string, string] =>
|
|
873
1039
|
typeof entry[1] === "string",
|
|
874
1040
|
),
|
|
875
1041
|
),
|
|
876
1042
|
}
|
|
877
1043
|
: {}),
|
|
878
|
-
...(isRecord(
|
|
1044
|
+
...(isRecord(mailboxSource)
|
|
879
1045
|
? {
|
|
880
1046
|
mailbox: {
|
|
881
|
-
...(Array.isArray(
|
|
1047
|
+
...(Array.isArray(mailboxSource.accepts)
|
|
882
1048
|
? {
|
|
883
|
-
accepts:
|
|
1049
|
+
accepts: mailboxSource.accepts.filter(
|
|
884
1050
|
(value): value is TemplateRecipeMailboxEntry =>
|
|
885
1051
|
typeof value === "string" ||
|
|
886
1052
|
(isRecord(value) && typeof value.type === "string"),
|
|
887
1053
|
),
|
|
888
1054
|
}
|
|
889
1055
|
: {}),
|
|
890
|
-
...(Array.isArray(
|
|
1056
|
+
...(Array.isArray(mailboxSource.emits)
|
|
891
1057
|
? {
|
|
892
|
-
emits:
|
|
1058
|
+
emits: mailboxSource.emits.filter(
|
|
893
1059
|
(value): value is TemplateRecipeMailboxEntry =>
|
|
894
1060
|
typeof value === "string" ||
|
|
895
1061
|
(isRecord(value) && typeof value.type === "string"),
|
|
@@ -899,7 +1065,8 @@ export function readResolvedRecipeConfig(
|
|
|
899
1065
|
},
|
|
900
1066
|
}
|
|
901
1067
|
: {}),
|
|
902
|
-
...(substituted.retire_when === "children_terminal"
|
|
1068
|
+
...(substituted.retire_when === "children_terminal" ||
|
|
1069
|
+
delegated?.retire_when === "children_terminal"
|
|
903
1070
|
? { retire_when: "children_terminal" as const }
|
|
904
1071
|
: {}),
|
|
905
1072
|
...(typeof substituted.retry === "number" ||
|
package/lib/registry.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface RegisterToolResult {
|
|
|
50
50
|
export interface RegisterToolRuntimeDeps<TContext> {
|
|
51
51
|
configPath: string;
|
|
52
52
|
recipeRoot?: string;
|
|
53
|
-
|
|
53
|
+
getToolNameBlocker: (name: string) => string | undefined;
|
|
54
54
|
getTools: () => Map<string, Config.RegisteredTool>;
|
|
55
55
|
getActiveTools: () => string[];
|
|
56
56
|
notify: (
|
|
@@ -132,8 +132,8 @@ function promoteDraftRecipe<TContext>(
|
|
|
132
132
|
const targetPath = getToolRecipePath(deps, name);
|
|
133
133
|
const tools = deps.getTools();
|
|
134
134
|
const existing = tools.get(name);
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
135
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
136
|
+
if (blocker) throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
137
137
|
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
138
138
|
throw new Error(
|
|
139
139
|
ExecutionOutput.formatToolText(
|
|
@@ -383,8 +383,8 @@ export async function executeRegisterTool<TContext>(
|
|
|
383
383
|
return deleteTool(name, ctx, deps);
|
|
384
384
|
const tools = deps.getTools();
|
|
385
385
|
const existing = tools.get(name);
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
386
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
387
|
+
if (blocker) throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
388
388
|
if (existing && !input.update) {
|
|
389
389
|
throw new Error(
|
|
390
390
|
ExecutionOutput.formatToolText(
|
package/lib/runtime-notifier.ts
CHANGED
|
@@ -147,6 +147,7 @@ export function createFileRuntimeNotifier(
|
|
|
147
147
|
mkdirSync(dirname(file), { recursive: true });
|
|
148
148
|
let position =
|
|
149
149
|
options.replay || !existsSync(file) ? 0 : statSync(file).size;
|
|
150
|
+
let pending = "";
|
|
150
151
|
let closed = false;
|
|
151
152
|
const reconcile = (reason: RuntimeReconcileReason): void => {
|
|
152
153
|
if (closed) return;
|
|
@@ -160,10 +161,15 @@ export function createFileRuntimeNotifier(
|
|
|
160
161
|
const drain = (): void => {
|
|
161
162
|
if (closed || !existsSync(file)) return;
|
|
162
163
|
const buffer = readFileSync(file);
|
|
163
|
-
if (position > buffer.length)
|
|
164
|
-
|
|
164
|
+
if (position > buffer.length) {
|
|
165
|
+
position = 0;
|
|
166
|
+
pending = "";
|
|
167
|
+
}
|
|
168
|
+
const chunk = pending + buffer.subarray(position).toString("utf8");
|
|
165
169
|
position = buffer.length;
|
|
166
|
-
|
|
170
|
+
const lines = chunk.split("\n");
|
|
171
|
+
pending = chunk.endsWith("\n") ? "" : (lines.pop() ?? "");
|
|
172
|
+
for (const line of lines) {
|
|
167
173
|
if (!line.trim()) continue;
|
|
168
174
|
const event = parseRuntimeWakeEventLine(line);
|
|
169
175
|
if (event && event.actor === actor) {
|
package/lib/runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool registry runtime coordinator
|
|
3
3
|
* Zones: runtime coordination, registry loading, pi tools
|
|
4
|
-
* Owns persisted tool loading,
|
|
4
|
+
* Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
@@ -19,17 +19,12 @@ export interface RuntimeContext {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export interface ToolInfoLike {
|
|
23
|
-
name: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
export interface ToolRegistryRuntimeDeps {
|
|
27
23
|
configPath: string;
|
|
28
24
|
exec: RegisteredToolExec;
|
|
29
25
|
packagedRecipeRoot?: string;
|
|
30
26
|
recipeRoot?: string;
|
|
31
27
|
getActiveTools?: () => string[];
|
|
32
|
-
getAllTools: () => ToolInfoLike[];
|
|
33
28
|
registerTool: (
|
|
34
29
|
definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
|
|
35
30
|
) => void;
|
|
@@ -38,7 +33,7 @@ export interface ToolRegistryRuntimeDeps {
|
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
export interface ToolRegistryRuntime {
|
|
41
|
-
|
|
36
|
+
getToolNameBlocker(name: string): string | undefined;
|
|
42
37
|
getTools(): Map<string, Config.RegisteredTool>;
|
|
43
38
|
loadTools(ctx: RuntimeContext): void;
|
|
44
39
|
notify(
|
|
@@ -67,11 +62,9 @@ export function createAutoToolsRuntime(
|
|
|
67
62
|
) {
|
|
68
63
|
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
69
64
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return existing
|
|
74
|
-
? `Tool "${name}" is already registered outside pi-actors.`
|
|
65
|
+
function getToolNameBlocker(name: string): string | undefined {
|
|
66
|
+
return deps.reservedToolNames.has(name)
|
|
67
|
+
? `Reserved tool name: ${name}`
|
|
75
68
|
: undefined;
|
|
76
69
|
}
|
|
77
70
|
function getToolFingerprint(cfg: Config.RegisteredTool): string {
|
|
@@ -105,6 +98,7 @@ export function createAutoToolsRuntime(
|
|
|
105
98
|
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
106
99
|
}
|
|
107
100
|
function isStartupActionableRegistryWarning(warning: string): boolean {
|
|
101
|
+
if (warning.includes(" shadows ")) return false;
|
|
108
102
|
if (
|
|
109
103
|
warning.includes("invokes bash;") &&
|
|
110
104
|
warning.includes("trusted executable content")
|
|
@@ -166,9 +160,9 @@ export function createAutoToolsRuntime(
|
|
|
166
160
|
}
|
|
167
161
|
deactivateMissingRuntimeTools(new Set(tools.keys()));
|
|
168
162
|
for (const cfg of tools.values()) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
warnings.push(
|
|
163
|
+
const blocker = getToolNameBlocker(cfg.name);
|
|
164
|
+
if (blocker) {
|
|
165
|
+
warnings.push(blocker);
|
|
172
166
|
continue;
|
|
173
167
|
}
|
|
174
168
|
registerRuntimeTool(cfg);
|
|
@@ -179,7 +173,7 @@ export function createAutoToolsRuntime(
|
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
175
|
return {
|
|
182
|
-
|
|
176
|
+
getToolNameBlocker,
|
|
183
177
|
getTools: () => tools,
|
|
184
178
|
loadTools,
|
|
185
179
|
notify,
|