@aigne/agent-runtime 1.12.0-beta.5
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/LICENSE.md +26 -0
- package/README.md +79 -0
- package/dist/agent-runners/ai-runner.cjs +133 -0
- package/dist/agent-runners/ai-runner.d.cts +33 -0
- package/dist/agent-runners/ai-runner.d.cts.map +1 -0
- package/dist/agent-runners/ai-runner.d.mts +33 -0
- package/dist/agent-runners/ai-runner.d.mts.map +1 -0
- package/dist/agent-runners/ai-runner.mjs +134 -0
- package/dist/agent-runners/ai-runner.mjs.map +1 -0
- package/dist/agent-runners/ash-runner.cjs +272 -0
- package/dist/agent-runners/ash-runner.d.cts +30 -0
- package/dist/agent-runners/ash-runner.d.cts.map +1 -0
- package/dist/agent-runners/ash-runner.d.mts +30 -0
- package/dist/agent-runners/ash-runner.d.mts.map +1 -0
- package/dist/agent-runners/ash-runner.mjs +273 -0
- package/dist/agent-runners/ash-runner.mjs.map +1 -0
- package/dist/agent-runners/budget.cjs +71 -0
- package/dist/agent-runners/budget.d.cts +40 -0
- package/dist/agent-runners/budget.d.cts.map +1 -0
- package/dist/agent-runners/budget.d.mts +40 -0
- package/dist/agent-runners/budget.d.mts.map +1 -0
- package/dist/agent-runners/budget.mjs +67 -0
- package/dist/agent-runners/budget.mjs.map +1 -0
- package/dist/agent-runners/code-runner.cjs +61 -0
- package/dist/agent-runners/code-runner.mjs +61 -0
- package/dist/agent-runners/code-runner.mjs.map +1 -0
- package/dist/agent-runners/env-whitelist.cjs +36 -0
- package/dist/agent-runners/env-whitelist.d.cts +5 -0
- package/dist/agent-runners/env-whitelist.d.cts.map +1 -0
- package/dist/agent-runners/env-whitelist.d.mts +5 -0
- package/dist/agent-runners/env-whitelist.d.mts.map +1 -0
- package/dist/agent-runners/env-whitelist.mjs +36 -0
- package/dist/agent-runners/env-whitelist.mjs.map +1 -0
- package/dist/agent-runners/guide-rails.cjs +47 -0
- package/dist/agent-runners/guide-rails.d.cts +16 -0
- package/dist/agent-runners/guide-rails.d.cts.map +1 -0
- package/dist/agent-runners/guide-rails.d.mts +16 -0
- package/dist/agent-runners/guide-rails.d.mts.map +1 -0
- package/dist/agent-runners/guide-rails.mjs +48 -0
- package/dist/agent-runners/guide-rails.mjs.map +1 -0
- package/dist/agent-runners/hook-channel.cjs +55 -0
- package/dist/agent-runners/hook-channel.d.cts +38 -0
- package/dist/agent-runners/hook-channel.d.cts.map +1 -0
- package/dist/agent-runners/hook-channel.d.mts +38 -0
- package/dist/agent-runners/hook-channel.d.mts.map +1 -0
- package/dist/agent-runners/hook-channel.mjs +55 -0
- package/dist/agent-runners/hook-channel.mjs.map +1 -0
- package/dist/agent-runners/hooks.cjs +107 -0
- package/dist/agent-runners/hooks.d.cts +41 -0
- package/dist/agent-runners/hooks.d.cts.map +1 -0
- package/dist/agent-runners/hooks.d.mts +41 -0
- package/dist/agent-runners/hooks.d.mts.map +1 -0
- package/dist/agent-runners/hooks.mjs +107 -0
- package/dist/agent-runners/hooks.mjs.map +1 -0
- package/dist/agent-runners/memory-afs.cjs +342 -0
- package/dist/agent-runners/memory-afs.d.cts +221 -0
- package/dist/agent-runners/memory-afs.d.cts.map +1 -0
- package/dist/agent-runners/memory-afs.d.mts +221 -0
- package/dist/agent-runners/memory-afs.d.mts.map +1 -0
- package/dist/agent-runners/memory-afs.mjs +326 -0
- package/dist/agent-runners/memory-afs.mjs.map +1 -0
- package/dist/agent-runners/memory-archive.cjs +263 -0
- package/dist/agent-runners/memory-archive.d.cts +47 -0
- package/dist/agent-runners/memory-archive.d.cts.map +1 -0
- package/dist/agent-runners/memory-archive.d.mts +47 -0
- package/dist/agent-runners/memory-archive.d.mts.map +1 -0
- package/dist/agent-runners/memory-archive.mjs +257 -0
- package/dist/agent-runners/memory-archive.mjs.map +1 -0
- package/dist/agent-runners/orchestrator-runner.cjs +302 -0
- package/dist/agent-runners/orchestrator-runner.d.cts +23 -0
- package/dist/agent-runners/orchestrator-runner.d.cts.map +1 -0
- package/dist/agent-runners/orchestrator-runner.d.mts +23 -0
- package/dist/agent-runners/orchestrator-runner.d.mts.map +1 -0
- package/dist/agent-runners/orchestrator-runner.mjs +303 -0
- package/dist/agent-runners/orchestrator-runner.mjs.map +1 -0
- package/dist/agent-runners/orchestrator-state.cjs +68 -0
- package/dist/agent-runners/orchestrator-state.d.cts +23 -0
- package/dist/agent-runners/orchestrator-state.d.cts.map +1 -0
- package/dist/agent-runners/orchestrator-state.d.mts +23 -0
- package/dist/agent-runners/orchestrator-state.d.mts.map +1 -0
- package/dist/agent-runners/orchestrator-state.mjs +65 -0
- package/dist/agent-runners/orchestrator-state.mjs.map +1 -0
- package/dist/agent-runners/retry.cjs +30 -0
- package/dist/agent-runners/retry.d.cts +13 -0
- package/dist/agent-runners/retry.d.cts.map +1 -0
- package/dist/agent-runners/retry.d.mts +13 -0
- package/dist/agent-runners/retry.d.mts.map +1 -0
- package/dist/agent-runners/retry.mjs +30 -0
- package/dist/agent-runners/retry.mjs.map +1 -0
- package/dist/agent-runners/scratchpad-afs.cjs +276 -0
- package/dist/agent-runners/scratchpad-afs.d.cts +103 -0
- package/dist/agent-runners/scratchpad-afs.d.cts.map +1 -0
- package/dist/agent-runners/scratchpad-afs.d.mts +103 -0
- package/dist/agent-runners/scratchpad-afs.d.mts.map +1 -0
- package/dist/agent-runners/scratchpad-afs.mjs +271 -0
- package/dist/agent-runners/scratchpad-afs.mjs.map +1 -0
- package/dist/agent-runners/stream-router.cjs +72 -0
- package/dist/agent-runners/stream-router.d.cts +22 -0
- package/dist/agent-runners/stream-router.d.cts.map +1 -0
- package/dist/agent-runners/stream-router.d.mts +22 -0
- package/dist/agent-runners/stream-router.d.mts.map +1 -0
- package/dist/agent-runners/stream-router.mjs +72 -0
- package/dist/agent-runners/stream-router.mjs.map +1 -0
- package/dist/agent-runners/system-prompt.cjs +35 -0
- package/dist/agent-runners/system-prompt.d.cts +17 -0
- package/dist/agent-runners/system-prompt.d.cts.map +1 -0
- package/dist/agent-runners/system-prompt.d.mts +17 -0
- package/dist/agent-runners/system-prompt.d.mts.map +1 -0
- package/dist/agent-runners/system-prompt.mjs +34 -0
- package/dist/agent-runners/system-prompt.mjs.map +1 -0
- package/dist/agent-runners/team-modes/aggregates.cjs +36 -0
- package/dist/agent-runners/team-modes/aggregates.mjs +35 -0
- package/dist/agent-runners/team-modes/aggregates.mjs.map +1 -0
- package/dist/agent-runners/team-modes/iterate.cjs +46 -0
- package/dist/agent-runners/team-modes/iterate.mjs +47 -0
- package/dist/agent-runners/team-modes/iterate.mjs.map +1 -0
- package/dist/agent-runners/team-modes/parallel.cjs +59 -0
- package/dist/agent-runners/team-modes/parallel.mjs +60 -0
- package/dist/agent-runners/team-modes/parallel.mjs.map +1 -0
- package/dist/agent-runners/team-modes/reflection.cjs +155 -0
- package/dist/agent-runners/team-modes/reflection.mjs +156 -0
- package/dist/agent-runners/team-modes/reflection.mjs.map +1 -0
- package/dist/agent-runners/team-modes/sequential.cjs +63 -0
- package/dist/agent-runners/team-modes/sequential.mjs +64 -0
- package/dist/agent-runners/team-modes/sequential.mjs.map +1 -0
- package/dist/agent-runners/team-modes/utils.cjs +39 -0
- package/dist/agent-runners/team-modes/utils.mjs +36 -0
- package/dist/agent-runners/team-modes/utils.mjs.map +1 -0
- package/dist/agent-runners/team-runner.cjs +277 -0
- package/dist/agent-runners/team-runner.d.cts +34 -0
- package/dist/agent-runners/team-runner.d.cts.map +1 -0
- package/dist/agent-runners/team-runner.d.mts +34 -0
- package/dist/agent-runners/team-runner.d.mts.map +1 -0
- package/dist/agent-runners/team-runner.mjs +278 -0
- package/dist/agent-runners/team-runner.mjs.map +1 -0
- package/dist/agent-runners/template-context.cjs +32 -0
- package/dist/agent-runners/template-context.d.cts +43 -0
- package/dist/agent-runners/template-context.d.cts.map +1 -0
- package/dist/agent-runners/template-context.d.mts +43 -0
- package/dist/agent-runners/template-context.d.mts.map +1 -0
- package/dist/agent-runners/template-context.mjs +33 -0
- package/dist/agent-runners/template-context.mjs.map +1 -0
- package/dist/index.cjs +123 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.mjs +29 -0
- package/dist/program/agent-action.cjs +496 -0
- package/dist/program/agent-action.d.cts +42 -0
- package/dist/program/agent-action.d.cts.map +1 -0
- package/dist/program/agent-action.d.mts +42 -0
- package/dist/program/agent-action.d.mts.map +1 -0
- package/dist/program/agent-action.mjs +497 -0
- package/dist/program/agent-action.mjs.map +1 -0
- package/dist/program/agent-messages-tools.cjs +137 -0
- package/dist/program/agent-messages-tools.d.cts +59 -0
- package/dist/program/agent-messages-tools.d.cts.map +1 -0
- package/dist/program/agent-messages-tools.d.mts +59 -0
- package/dist/program/agent-messages-tools.d.mts.map +1 -0
- package/dist/program/agent-messages-tools.mjs +130 -0
- package/dist/program/agent-messages-tools.mjs.map +1 -0
- package/dist/program/agent-runner-registry.cjs +314 -0
- package/dist/program/agent-runner-registry.d.cts +52 -0
- package/dist/program/agent-runner-registry.d.cts.map +1 -0
- package/dist/program/agent-runner-registry.d.mts +52 -0
- package/dist/program/agent-runner-registry.d.mts.map +1 -0
- package/dist/program/agent-runner-registry.mjs +313 -0
- package/dist/program/agent-runner-registry.mjs.map +1 -0
- package/dist/program/agent-subscribe.cjs +72 -0
- package/dist/program/agent-subscribe.d.cts +18 -0
- package/dist/program/agent-subscribe.d.cts.map +1 -0
- package/dist/program/agent-subscribe.d.mts +18 -0
- package/dist/program/agent-subscribe.d.mts.map +1 -0
- package/dist/program/agent-subscribe.mjs +73 -0
- package/dist/program/agent-subscribe.mjs.map +1 -0
- package/dist/program/agent-tools.cjs +135 -0
- package/dist/program/agent-tools.d.cts +35 -0
- package/dist/program/agent-tools.d.cts.map +1 -0
- package/dist/program/agent-tools.d.mts +35 -0
- package/dist/program/agent-tools.d.mts.map +1 -0
- package/dist/program/agent-tools.mjs +136 -0
- package/dist/program/agent-tools.mjs.map +1 -0
- package/dist/program/manifest-nesting.cjs +57 -0
- package/dist/program/manifest-nesting.d.cts +13 -0
- package/dist/program/manifest-nesting.d.cts.map +1 -0
- package/dist/program/manifest-nesting.d.mts +13 -0
- package/dist/program/manifest-nesting.d.mts.map +1 -0
- package/dist/program/manifest-nesting.mjs +57 -0
- package/dist/program/manifest-nesting.mjs.map +1 -0
- package/dist/program/result-contract.cjs +55 -0
- package/dist/program/result-contract.d.cts +23 -0
- package/dist/program/result-contract.d.cts.map +1 -0
- package/dist/program/result-contract.d.mts +23 -0
- package/dist/program/result-contract.d.mts.map +1 -0
- package/dist/program/result-contract.mjs +51 -0
- package/dist/program/result-contract.mjs.map +1 -0
- package/dist/program/runner-error.cjs +71 -0
- package/dist/program/runner-error.d.cts +25 -0
- package/dist/program/runner-error.d.cts.map +1 -0
- package/dist/program/runner-error.d.mts +25 -0
- package/dist/program/runner-error.d.mts.map +1 -0
- package/dist/program/runner-error.mjs +72 -0
- package/dist/program/runner-error.mjs.map +1 -0
- package/dist/program/scoped-afs.cjs +538 -0
- package/dist/program/scoped-afs.d.cts +245 -0
- package/dist/program/scoped-afs.d.cts.map +1 -0
- package/dist/program/scoped-afs.d.mts +245 -0
- package/dist/program/scoped-afs.d.mts.map +1 -0
- package/dist/program/scoped-afs.mjs +531 -0
- package/dist/program/scoped-afs.mjs.map +1 -0
- package/dist/program/session-hierarchy.cjs +1 -0
- package/dist/program/session-hierarchy.d.cts +2 -0
- package/dist/program/session-hierarchy.d.mts +2 -0
- package/dist/program/session-hierarchy.mjs +3 -0
- package/dist/program/tool-discovery/ash-frontmatter.cjs +83 -0
- package/dist/program/tool-discovery/ash-frontmatter.d.cts +21 -0
- package/dist/program/tool-discovery/ash-frontmatter.d.cts.map +1 -0
- package/dist/program/tool-discovery/ash-frontmatter.d.mts +21 -0
- package/dist/program/tool-discovery/ash-frontmatter.d.mts.map +1 -0
- package/dist/program/tool-discovery/ash-frontmatter.mjs +84 -0
- package/dist/program/tool-discovery/ash-frontmatter.mjs.map +1 -0
- package/dist/program/tool-discovery/dot-path-resolver.cjs +60 -0
- package/dist/program/tool-discovery/dot-path-resolver.mjs +61 -0
- package/dist/program/tool-discovery/dot-path-resolver.mjs.map +1 -0
- package/dist/program/tool-discovery/skill-gate.cjs +36 -0
- package/dist/program/tool-discovery/skill-gate.d.cts +10 -0
- package/dist/program/tool-discovery/skill-gate.d.cts.map +1 -0
- package/dist/program/tool-discovery/skill-gate.d.mts +10 -0
- package/dist/program/tool-discovery/skill-gate.d.mts.map +1 -0
- package/dist/program/tool-discovery/skill-gate.mjs +35 -0
- package/dist/program/tool-discovery/skill-gate.mjs.map +1 -0
- package/dist/providers/basic/ash/dist/index.cjs +1809 -0
- package/dist/providers/basic/ash/dist/index.mjs +1807 -0
- package/dist/providers/basic/ash/dist/index.mjs.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1807 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
2
|
+
import { AFSNotFoundError, SCRIPT_EVENT_TYPES, getPlatform, makeNsLog } from "@aigne/afs";
|
|
3
|
+
import { AFSBaseProvider, Actions, Delete, Exec, Explain, List, Meta, Read, Search, Stat, Write } from "@aigne/afs/provider";
|
|
4
|
+
import { ASH_REFERENCE, compileSource } from "@aigne/ash";
|
|
5
|
+
import { Cron, CronPattern } from "croner";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
//#region ../../providers/basic/ash/dist/index.mjs
|
|
9
|
+
function __decorate(decorators, target, key, desc) {
|
|
10
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read-class pairing: a grant of `list` on a data root implies `query` (the
|
|
17
|
+
* collection-query standard action) — both are readPaths-class observation
|
|
18
|
+
* privileges, and the agent default tooling rule says standard observation
|
|
19
|
+
* comes with the data root, not per-tool opt-in. Idempotent.
|
|
20
|
+
*/
|
|
21
|
+
function withQueryPairing(ops) {
|
|
22
|
+
if (ops.includes("list") && !ops.includes("query")) return [...ops, "query"];
|
|
23
|
+
return ops;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Match an actual path against a declaration pattern.
|
|
27
|
+
*
|
|
28
|
+
* Supports:
|
|
29
|
+
* - Exact: "/a/b" matches "/a/b"
|
|
30
|
+
* - Single-level `*`: "/a/*" matches "/a/b" but NOT "/a/b/c"
|
|
31
|
+
* - Multi-level `**`: "/a/**" matches "/a/b/c" up to `maxDepth` levels from `**` position
|
|
32
|
+
*
|
|
33
|
+
* @param maxDepth Maximum levels `**` can consume. Defaults to 0 (no expansion).
|
|
34
|
+
*/
|
|
35
|
+
function matchPath(actual, pattern, maxDepth = 0) {
|
|
36
|
+
if (actual === pattern) return true;
|
|
37
|
+
const patternParts = pattern.split("/");
|
|
38
|
+
const actualParts = actual.split("/");
|
|
39
|
+
const dstarIdx = patternParts.indexOf("**");
|
|
40
|
+
if (dstarIdx === -1) return matchSingleLevel(actualParts, patternParts);
|
|
41
|
+
const prefix = patternParts.slice(0, dstarIdx);
|
|
42
|
+
const suffix = patternParts.slice(dstarIdx + 1);
|
|
43
|
+
if (actualParts.length < prefix.length + suffix.length) return false;
|
|
44
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
45
|
+
if (prefix[i] === "*") continue;
|
|
46
|
+
if (prefix[i] !== actualParts[i]) return false;
|
|
47
|
+
}
|
|
48
|
+
const suffixStart = actualParts.length - suffix.length;
|
|
49
|
+
if (suffixStart < prefix.length) return false;
|
|
50
|
+
for (let i = 0; i < suffix.length; i++) {
|
|
51
|
+
if (suffix[i] === "*") continue;
|
|
52
|
+
if (suffix[i] !== actualParts[suffixStart + i]) return false;
|
|
53
|
+
}
|
|
54
|
+
const dstarLevels = actualParts.length - prefix.length - suffix.length;
|
|
55
|
+
return dstarLevels >= 0 && dstarLevels <= maxDepth;
|
|
56
|
+
}
|
|
57
|
+
/** Single-level * matching (no **). */
|
|
58
|
+
function matchSingleLevel(actualParts, patternParts) {
|
|
59
|
+
if (!patternParts.some((p) => p === "*")) return false;
|
|
60
|
+
if (actualParts.length === patternParts.length) {
|
|
61
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
62
|
+
if (patternParts[i] === "*") continue;
|
|
63
|
+
if (patternParts[i] !== actualParts[i]) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (actualParts.length < patternParts.length) {
|
|
68
|
+
for (let i = actualParts.length; i < patternParts.length; i++) if (patternParts[i] !== "*") return false;
|
|
69
|
+
for (let i = 0; i < actualParts.length; i++) if (patternParts[i] !== "*" && patternParts[i] !== actualParts[i]) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Extract the action name from an exec path.
|
|
76
|
+
* E.g., "/modules/github/issues/42/.actions/close-issue" → "close-issue"
|
|
77
|
+
*/
|
|
78
|
+
function extractActionName(path) {
|
|
79
|
+
const idx = path.lastIndexOf("/.actions/");
|
|
80
|
+
if (idx === -1) return void 0;
|
|
81
|
+
return path.slice(idx + 10).split("/")[0];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate a tool_call's path and operation against the tools declaration.
|
|
85
|
+
*/
|
|
86
|
+
function validateToolCall(path, op, tools) {
|
|
87
|
+
if (hasUnsafePathSyntax(path)) return {
|
|
88
|
+
allowed: false,
|
|
89
|
+
reason: `Unsafe path syntax in '${path}'`
|
|
90
|
+
};
|
|
91
|
+
let basePath;
|
|
92
|
+
if (op === "exec") {
|
|
93
|
+
const idx = path.indexOf("/.actions/");
|
|
94
|
+
if (idx !== -1) basePath = path.slice(0, idx);
|
|
95
|
+
}
|
|
96
|
+
let pathMatched = false;
|
|
97
|
+
for (const entry of tools) {
|
|
98
|
+
const md = entry.maxDepth ?? 0;
|
|
99
|
+
if (!matchPath(path, entry.path, md) && !(basePath && matchPath(basePath, entry.path, md))) continue;
|
|
100
|
+
pathMatched = true;
|
|
101
|
+
if (!entry.ops.includes(op)) continue;
|
|
102
|
+
if (op === "exec" && entry.exclude_actions?.length) {
|
|
103
|
+
const actionName = extractActionName(path);
|
|
104
|
+
if (actionName && entry.exclude_actions.includes(actionName)) return {
|
|
105
|
+
allowed: false,
|
|
106
|
+
reason: `Action '${actionName}' is excluded on '${entry.path}'`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { allowed: true };
|
|
110
|
+
}
|
|
111
|
+
if (pathMatched) return {
|
|
112
|
+
allowed: false,
|
|
113
|
+
reason: `Operation '${op}' not allowed on '${path}'`
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
allowed: false,
|
|
117
|
+
reason: `Path '${path}' not in tools declaration`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function hasUnsafePathSyntax(path) {
|
|
121
|
+
if (path.includes("..") || path.includes("{") || path.includes("\\") || path.includes("\0")) return true;
|
|
122
|
+
try {
|
|
123
|
+
const decoded = decodeURIComponent(path);
|
|
124
|
+
return decoded !== path && hasUnsafePathSyntax(decoded);
|
|
125
|
+
} catch {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isChildPath(childPath, parentPath, parentMaxDepth) {
|
|
130
|
+
if (childPath === parentPath) return true;
|
|
131
|
+
const childHasGlob = childPath.includes("**") || childPath.includes("*");
|
|
132
|
+
const parentHasGlob = parentPath.includes("**") || parentPath.includes("*");
|
|
133
|
+
if (!parentHasGlob && !childHasGlob) return childPath === parentPath;
|
|
134
|
+
if (parentHasGlob && !childHasGlob) return matchPath(childPath, parentPath, parentMaxDepth);
|
|
135
|
+
if (parentHasGlob && childHasGlob) {
|
|
136
|
+
const parentBase = parentPath.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
137
|
+
const childBase = childPath.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
138
|
+
return childBase === parentBase || childBase.startsWith(`${parentBase}/`);
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Intersect two ToolEntry[] arrays. Returns the effective tool list for a
|
|
144
|
+
* child agent: only entries that both parent and child declare, with ops
|
|
145
|
+
* narrowed to the intersection and maxDepth capped by the parent.
|
|
146
|
+
*
|
|
147
|
+
* Violations are silently absent (no errors). This is a pure function.
|
|
148
|
+
*/
|
|
149
|
+
function intersectTools(parent, child) {
|
|
150
|
+
if (parent.length === 0 || child.length === 0) return [];
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const c of child) {
|
|
153
|
+
if (hasUnsafePathSyntax(c.path)) continue;
|
|
154
|
+
let bestParent = null;
|
|
155
|
+
for (const p of parent) {
|
|
156
|
+
if (hasUnsafePathSyntax(p.path)) continue;
|
|
157
|
+
if (!isChildPath(c.path, p.path, p.maxDepth ?? 0)) continue;
|
|
158
|
+
bestParent = p;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (!bestParent) continue;
|
|
162
|
+
const ops = c.ops.filter((op) => bestParent.ops.includes(op));
|
|
163
|
+
if (ops.length === 0) continue;
|
|
164
|
+
const entry = {
|
|
165
|
+
path: c.path,
|
|
166
|
+
ops
|
|
167
|
+
};
|
|
168
|
+
if (c.path.includes("**")) {
|
|
169
|
+
const parentDepth = bestParent.maxDepth ?? 0;
|
|
170
|
+
const childDepth = c.maxDepth ?? 0;
|
|
171
|
+
entry.maxDepth = Math.min(parentDepth, childDepth);
|
|
172
|
+
} else if (c.maxDepth != null) entry.maxDepth = bestParent.maxDepth != null ? Math.min(bestParent.maxDepth, c.maxDepth) : c.maxDepth;
|
|
173
|
+
const excludes = /* @__PURE__ */ new Set();
|
|
174
|
+
if (bestParent.exclude_actions) for (const a of bestParent.exclude_actions) excludes.add(a);
|
|
175
|
+
if (c.exclude_actions) for (const a of c.exclude_actions) excludes.add(a);
|
|
176
|
+
if (excludes.size > 0) entry.exclude_actions = [...excludes];
|
|
177
|
+
result.push(entry);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validate an ash action path against the manifest's tools whitelist.
|
|
183
|
+
* Used by the ash runner to enforce that compiled ash scripts only call
|
|
184
|
+
* paths permitted by the agent manifest.
|
|
185
|
+
*
|
|
186
|
+
* Returns { allowed: true } or { allowed: false, code, path, reason }.
|
|
187
|
+
*/
|
|
188
|
+
function validateActionPath(actionPath, tools) {
|
|
189
|
+
if (hasUnsafePathSyntax(actionPath)) return {
|
|
190
|
+
allowed: false,
|
|
191
|
+
code: "PATH_TRAVERSAL",
|
|
192
|
+
path: actionPath,
|
|
193
|
+
reason: `Unsafe path syntax detected in '${actionPath}'`
|
|
194
|
+
};
|
|
195
|
+
if (tools.length === 0) return {
|
|
196
|
+
allowed: false,
|
|
197
|
+
code: "TOOL_WHITELIST_VIOLATION",
|
|
198
|
+
path: actionPath,
|
|
199
|
+
reason: `No tools declared — action '${actionPath}' is not permitted`
|
|
200
|
+
};
|
|
201
|
+
const result = validateToolCall(actionPath, "exec", tools);
|
|
202
|
+
if (!result.allowed) return {
|
|
203
|
+
allowed: false,
|
|
204
|
+
code: "TOOL_WHITELIST_VIOLATION",
|
|
205
|
+
path: actionPath,
|
|
206
|
+
reason: result.reason
|
|
207
|
+
};
|
|
208
|
+
return { allowed: true };
|
|
209
|
+
}
|
|
210
|
+
const log = makeNsLog("provider:ash");
|
|
211
|
+
/** Lazily resolved docs directory path */
|
|
212
|
+
let _docsDir;
|
|
213
|
+
function getDocsDir() {
|
|
214
|
+
if (!_docsDir) {
|
|
215
|
+
const platform = getPlatform();
|
|
216
|
+
const moduleDir = platform.path.dirname(new URL(import.meta.url).pathname);
|
|
217
|
+
_docsDir = platform.path.join(moduleDir, "..", "docs");
|
|
218
|
+
}
|
|
219
|
+
return _docsDir;
|
|
220
|
+
}
|
|
221
|
+
async function readDoc(relativePath) {
|
|
222
|
+
const platform = getPlatform();
|
|
223
|
+
return platform.fs.readTextFile(platform.path.join(getDocsDir(), relativePath));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Dedup deprecation warning for the legacy `/ash/.actions/agent-run`
|
|
227
|
+
* alias — log once per process, not on every call.
|
|
228
|
+
*/
|
|
229
|
+
let ashAgentRunDeprecationLogged = false;
|
|
230
|
+
function sanitizeErrorPath(msg) {
|
|
231
|
+
return msg.replace(/'\/[^']*'/g, "'[path]'").replace(/"\/[^"]*"/g, "\"[path]\"");
|
|
232
|
+
}
|
|
233
|
+
function normalizeAbsoluteAfsPath(path) {
|
|
234
|
+
if (!path.startsWith("/")) return path;
|
|
235
|
+
const segments = [];
|
|
236
|
+
for (const segment of path.split("/")) {
|
|
237
|
+
if (!segment || segment === ".") continue;
|
|
238
|
+
if (segment === "..") segments.pop();
|
|
239
|
+
else segments.push(segment);
|
|
240
|
+
}
|
|
241
|
+
return `/${segments.join("/")}`;
|
|
242
|
+
}
|
|
243
|
+
async function writeFileAtomic(path, data) {
|
|
244
|
+
const platform = getPlatform();
|
|
245
|
+
const fs = platform.fs;
|
|
246
|
+
if (!fs.rename) {
|
|
247
|
+
await fs.writeFile(path, data);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const dir = platform.path.dirname(path);
|
|
251
|
+
const base = platform.path.basename(path);
|
|
252
|
+
const suffix = platform.crypto?.randomUUID() ?? `${Date.now()}-${Math.random()}`;
|
|
253
|
+
const tmpPath = platform.path.join(dir, `.${base}.${suffix}.tmp`);
|
|
254
|
+
await fs.writeFile(tmpPath, data);
|
|
255
|
+
try {
|
|
256
|
+
await fs.rename(tmpPath, path);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
await fs.rm(tmpPath).catch(() => {});
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Normalize an entry's content into a flat record for ASH pipeline access.
|
|
264
|
+
*
|
|
265
|
+
* Object content is spread so scripts can reference fields directly
|
|
266
|
+
* (`${state}`, `${attributes.temperature}`, etc.). String / null / array
|
|
267
|
+
* content is returned as-is under `content` — providers that need structured
|
|
268
|
+
* field access are responsible for returning objects from their read path
|
|
269
|
+
* (e.g. JSON, markdown, registry providers all do this).
|
|
270
|
+
*/
|
|
271
|
+
function normalizeContent(content) {
|
|
272
|
+
if (content == null) return {};
|
|
273
|
+
if (typeof content === "object" && !Array.isArray(content)) return {
|
|
274
|
+
content,
|
|
275
|
+
...content
|
|
276
|
+
};
|
|
277
|
+
return { content };
|
|
278
|
+
}
|
|
279
|
+
function normalizeAfsEntry(entry, fallbackPath) {
|
|
280
|
+
const path = entry.path ?? fallbackPath;
|
|
281
|
+
return {
|
|
282
|
+
path,
|
|
283
|
+
name: path.split("/").filter(Boolean).pop(),
|
|
284
|
+
...entry.meta,
|
|
285
|
+
...normalizeContent(entry.content)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function querySearchNeedle(query) {
|
|
289
|
+
if (!query || query.op !== "contains") return void 0;
|
|
290
|
+
if (query.field !== "content" && query.field !== "_content" && query.field !== "summary") return void 0;
|
|
291
|
+
if (Array.isArray(query.value)) return void 0;
|
|
292
|
+
const needle = String(query.value);
|
|
293
|
+
return needle === "" ? void 0 : needle;
|
|
294
|
+
}
|
|
295
|
+
async function readAfsRecords(afs, localWorld, path, query) {
|
|
296
|
+
const local = await localWorld.read(path, query);
|
|
297
|
+
if (local.length > 0) return local;
|
|
298
|
+
const searchNeedle = querySearchNeedle(query);
|
|
299
|
+
if (searchNeedle && afs.search) try {
|
|
300
|
+
const searchResult = await afs.search(path, searchNeedle, { limit: 1e3 });
|
|
301
|
+
if (searchResult.data.length > 0) return searchResult.data.map((entry) => normalizeAfsEntry(entry, path));
|
|
302
|
+
} catch {}
|
|
303
|
+
if (afs.list) try {
|
|
304
|
+
const listResult = await afs.list(path, query ? { query } : {});
|
|
305
|
+
if (listResult.data.length > 0) {
|
|
306
|
+
const needsEnrich = listResult.data.some((entry) => entry.content == null);
|
|
307
|
+
let entries = listResult.data;
|
|
308
|
+
if (needsEnrich && afs.read) {
|
|
309
|
+
const afsRead = afs.read.bind(afs);
|
|
310
|
+
entries = await Promise.all(listResult.data.map(async (entry) => {
|
|
311
|
+
if (entry.content != null) return entry;
|
|
312
|
+
try {
|
|
313
|
+
const result = await afsRead(entry.path, {});
|
|
314
|
+
return result.data ? {
|
|
315
|
+
...entry,
|
|
316
|
+
content: result.data.content
|
|
317
|
+
} : entry;
|
|
318
|
+
} catch {
|
|
319
|
+
return entry;
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
return entries.map((entry) => normalizeAfsEntry(entry, path));
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
if (afs.read) try {
|
|
327
|
+
const readResult = await afs.read(path, {});
|
|
328
|
+
if (readResult.data) return [normalizeAfsEntry(readResult.data, path)];
|
|
329
|
+
} catch {}
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* AFS ASH Provider
|
|
334
|
+
*
|
|
335
|
+
* Bridges ASH pipeline DSL to AFS. ASH scripts are stored in-memory and
|
|
336
|
+
* executed against AFS via a world bridge that translates ASH's
|
|
337
|
+
* read/write/publish to AFS read/write operations.
|
|
338
|
+
*
|
|
339
|
+
* Structure:
|
|
340
|
+
* - `/` - root directory
|
|
341
|
+
* - `/scripts` - stored .ash scripts
|
|
342
|
+
* - `/scripts/{name}.ash` - individual script files (executable)
|
|
343
|
+
* - `/.actions/run` - execute inline ASH code
|
|
344
|
+
* - `/.actions/validate` - validate ASH source (compile + type-check)
|
|
345
|
+
*/
|
|
346
|
+
var AFSAsh = class AFSAsh extends AFSBaseProvider {
|
|
347
|
+
static manifest() {
|
|
348
|
+
return {
|
|
349
|
+
name: "ash",
|
|
350
|
+
description: "ASH pipeline DSL for deterministic data pipelines.\n- Store, validate, and execute ASH scripts\n- Commands: find, where, map, save, publish, tee, fanout, output, input, count, group-by, action, route, lookup\n- Read /.meta/.ash-reference for full syntax",
|
|
351
|
+
uriTemplate: "ash://",
|
|
352
|
+
category: "compute",
|
|
353
|
+
agentMdPath: new URL("../docs/agent.md", import.meta.url).pathname,
|
|
354
|
+
schema: z.object({
|
|
355
|
+
name: z.string().optional(),
|
|
356
|
+
description: z.string().optional(),
|
|
357
|
+
allowRootActions: z.array(z.enum([
|
|
358
|
+
"agent-run",
|
|
359
|
+
"run",
|
|
360
|
+
"mount",
|
|
361
|
+
"unmount",
|
|
362
|
+
"read",
|
|
363
|
+
"list",
|
|
364
|
+
"write",
|
|
365
|
+
"delete"
|
|
366
|
+
])).optional()
|
|
367
|
+
}),
|
|
368
|
+
tags: [
|
|
369
|
+
"ash",
|
|
370
|
+
"pipeline",
|
|
371
|
+
"etl",
|
|
372
|
+
"dsl"
|
|
373
|
+
],
|
|
374
|
+
capabilityTags: [
|
|
375
|
+
"read-write",
|
|
376
|
+
"search",
|
|
377
|
+
"auth:none",
|
|
378
|
+
"local"
|
|
379
|
+
],
|
|
380
|
+
security: {
|
|
381
|
+
riskLevel: "system",
|
|
382
|
+
resourceAccess: ["process-spawn"],
|
|
383
|
+
dataSensitivity: ["code"],
|
|
384
|
+
notes: ["Executes ASH pipeline scripts that can read/write data across mounted providers"]
|
|
385
|
+
},
|
|
386
|
+
capabilities: { crossProvider: { afsAccess: true } }
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
static treeSchema() {
|
|
390
|
+
return {
|
|
391
|
+
operations: [
|
|
392
|
+
"list",
|
|
393
|
+
"read",
|
|
394
|
+
"write",
|
|
395
|
+
"delete",
|
|
396
|
+
"exec",
|
|
397
|
+
"search",
|
|
398
|
+
"stat",
|
|
399
|
+
"explain"
|
|
400
|
+
],
|
|
401
|
+
tree: {
|
|
402
|
+
"/": {
|
|
403
|
+
kind: "ash:root",
|
|
404
|
+
operations: [
|
|
405
|
+
"list",
|
|
406
|
+
"read",
|
|
407
|
+
"exec"
|
|
408
|
+
],
|
|
409
|
+
actions: ["run", "validate"]
|
|
410
|
+
},
|
|
411
|
+
"/scripts": {
|
|
412
|
+
kind: "ash:directory",
|
|
413
|
+
operations: ["list", "read"]
|
|
414
|
+
},
|
|
415
|
+
"/scripts/{name}.ash": {
|
|
416
|
+
kind: "ash:script",
|
|
417
|
+
operations: [
|
|
418
|
+
"read",
|
|
419
|
+
"write",
|
|
420
|
+
"delete",
|
|
421
|
+
"exec"
|
|
422
|
+
],
|
|
423
|
+
actions: ["exec"]
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
auth: { type: "none" },
|
|
427
|
+
bestFor: [
|
|
428
|
+
"data pipelines",
|
|
429
|
+
"ETL scripts",
|
|
430
|
+
"deterministic DSL"
|
|
431
|
+
],
|
|
432
|
+
notFor: ["general-purpose code"]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
static async load({ config } = {}) {
|
|
436
|
+
return new AFSAsh(config);
|
|
437
|
+
}
|
|
438
|
+
name;
|
|
439
|
+
description;
|
|
440
|
+
accessMode = "readwrite";
|
|
441
|
+
scripts = /* @__PURE__ */ new Map();
|
|
442
|
+
writeQueues = /* @__PURE__ */ new Map();
|
|
443
|
+
afsRoot;
|
|
444
|
+
scriptsDir;
|
|
445
|
+
dataDir;
|
|
446
|
+
allowRootActions;
|
|
447
|
+
triggerSubs = /* @__PURE__ */ new Map();
|
|
448
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
449
|
+
/** Cron registry for alarm-driven execution (CF Worker DOs where setTimeout is unreliable). */
|
|
450
|
+
cronRegistry = /* @__PURE__ */ new Map();
|
|
451
|
+
externalScripts = /* @__PURE__ */ new Map();
|
|
452
|
+
externalScriptSubs = [];
|
|
453
|
+
/** Pending trigger execution promises — for testing/awaiting async triggers. */
|
|
454
|
+
pendingTriggers = /* @__PURE__ */ new Set();
|
|
455
|
+
/** Per-script AFS override for program-scoped triggers (scriptName → program AFS). */
|
|
456
|
+
triggerAfsOverrides = /* @__PURE__ */ new Map();
|
|
457
|
+
/** Script trigger keys owned by each program trigger registration. */
|
|
458
|
+
programTriggerKeys = /* @__PURE__ */ new Map();
|
|
459
|
+
/** Namespace prefixes registered via registerProgramTriggers (e.g. "/blocklets/evan"). */
|
|
460
|
+
programNamespaces = /* @__PURE__ */ new Set();
|
|
461
|
+
/** Runtime AFSes promoted to program-AFS. When events arrive on these,
|
|
462
|
+
* the onMount-installed `initTriggers` subscription skips to avoid
|
|
463
|
+
* duplicate registration — `registerProgramTriggers` owns them. */
|
|
464
|
+
programAfses = /* @__PURE__ */ new WeakSet();
|
|
465
|
+
dirsReady = false;
|
|
466
|
+
constructor(options = {}) {
|
|
467
|
+
super();
|
|
468
|
+
this.name = options.name ?? "ash";
|
|
469
|
+
this.description = options.description ?? "ASH pipeline DSL for deterministic data pipelines";
|
|
470
|
+
this.scriptsDir = options.scriptsDir;
|
|
471
|
+
this.dataDir = options.dataDir;
|
|
472
|
+
const rawAllow = options.allowRootActions;
|
|
473
|
+
const allowList = Array.isArray(rawAllow) ? rawAllow : typeof rawAllow === "string" ? [rawAllow] : ["agent-run"];
|
|
474
|
+
const validActions = new Set([
|
|
475
|
+
"agent-run",
|
|
476
|
+
"run",
|
|
477
|
+
"mount",
|
|
478
|
+
"unmount",
|
|
479
|
+
"read",
|
|
480
|
+
"list",
|
|
481
|
+
"write",
|
|
482
|
+
"delete"
|
|
483
|
+
]);
|
|
484
|
+
const normalized = allowList.filter((a) => validActions.has(a));
|
|
485
|
+
this.allowRootActions = new Set(normalized.length > 0 ? normalized : ["agent-run"]);
|
|
486
|
+
}
|
|
487
|
+
async ready() {
|
|
488
|
+
if (this.dirsReady) return;
|
|
489
|
+
const fs = getPlatform().fs;
|
|
490
|
+
if (this.scriptsDir) await fs.mkdir(this.scriptsDir, { recursive: true });
|
|
491
|
+
if (this.dataDir) await fs.mkdir(this.dataDir, { recursive: true });
|
|
492
|
+
this.dirsReady = true;
|
|
493
|
+
}
|
|
494
|
+
async ensureDirs() {
|
|
495
|
+
if (!this.dirsReady) await this.ready();
|
|
496
|
+
}
|
|
497
|
+
async enqueueWrite(key, fn) {
|
|
498
|
+
const previous = this.writeQueues.get(key) ?? Promise.resolve();
|
|
499
|
+
let release;
|
|
500
|
+
const current = new Promise((resolve) => {
|
|
501
|
+
release = resolve;
|
|
502
|
+
});
|
|
503
|
+
this.writeQueues.set(key, previous.then(() => current, () => current));
|
|
504
|
+
await previous.catch(() => {});
|
|
505
|
+
try {
|
|
506
|
+
return await fn();
|
|
507
|
+
} finally {
|
|
508
|
+
release();
|
|
509
|
+
if (this.writeQueues.get(key) === current) this.writeQueues.delete(key);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async getScript(name) {
|
|
513
|
+
if (this.scriptsDir) {
|
|
514
|
+
const platform = getPlatform();
|
|
515
|
+
const filePath = platform.path.join(this.scriptsDir, `${name}.ash`);
|
|
516
|
+
try {
|
|
517
|
+
return {
|
|
518
|
+
source: await platform.fs.readTextFile(filePath),
|
|
519
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
520
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
521
|
+
};
|
|
522
|
+
} catch {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return this.scripts.get(name);
|
|
527
|
+
}
|
|
528
|
+
async setScript(name, script) {
|
|
529
|
+
if (this.scriptsDir) {
|
|
530
|
+
await this.ensureDirs();
|
|
531
|
+
await writeFileAtomic(getPlatform().path.join(this.scriptsDir, `${name}.ash`), script.source);
|
|
532
|
+
}
|
|
533
|
+
this.scripts.set(name, script);
|
|
534
|
+
}
|
|
535
|
+
async hasScript(name) {
|
|
536
|
+
if (this.scriptsDir) try {
|
|
537
|
+
const platform = getPlatform();
|
|
538
|
+
await platform.fs.stat(platform.path.join(this.scriptsDir, `${name}.ash`));
|
|
539
|
+
return true;
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return this.scripts.has(name);
|
|
544
|
+
}
|
|
545
|
+
async removeScript(name) {
|
|
546
|
+
if (this.scriptsDir) try {
|
|
547
|
+
const platform = getPlatform();
|
|
548
|
+
await platform.fs.rm(platform.path.join(this.scriptsDir, `${name}.ash`));
|
|
549
|
+
} catch {}
|
|
550
|
+
this.scripts.delete(name);
|
|
551
|
+
}
|
|
552
|
+
async getAllScriptNames() {
|
|
553
|
+
if (this.scriptsDir) try {
|
|
554
|
+
return (await getPlatform().fs.readdir(this.scriptsDir)).filter((f) => f.endsWith(".ash")).map((f) => f.replace(/\.ash$/, ""));
|
|
555
|
+
} catch {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
return Array.from(this.scripts.keys());
|
|
559
|
+
}
|
|
560
|
+
async getScriptCount() {
|
|
561
|
+
return (await this.getAllScriptNames()).length;
|
|
562
|
+
}
|
|
563
|
+
/** Sanitize an ASH path to a flat filename: /data/clean → data%2Fclean.json */
|
|
564
|
+
dataFileName(path) {
|
|
565
|
+
return `${path.replace(/^\/+/, "").replace(/\//g, "%2F")}.json`;
|
|
566
|
+
}
|
|
567
|
+
/** Persist save output to dataDir */
|
|
568
|
+
async persistData(path, data) {
|
|
569
|
+
if (!this.dataDir) return;
|
|
570
|
+
await this.enqueueWrite(`data:${path}`, async () => {
|
|
571
|
+
await this.ensureDirs();
|
|
572
|
+
await writeFileAtomic(getPlatform().path.join(this.dataDir, this.dataFileName(path)), JSON.stringify(data));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async updateScript(name, update) {
|
|
576
|
+
return this.enqueueWrite(`script:${name}`, async () => {
|
|
577
|
+
const next = await update(await this.getScript(name));
|
|
578
|
+
if (next) await this.setScript(name, next);
|
|
579
|
+
else await this.removeScript(name);
|
|
580
|
+
return next;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/** Load all persisted data files into a dataStore */
|
|
584
|
+
async loadPersistedData() {
|
|
585
|
+
const store = {};
|
|
586
|
+
if (!this.dataDir) return store;
|
|
587
|
+
const platform = getPlatform();
|
|
588
|
+
try {
|
|
589
|
+
const files = await platform.fs.readdir(this.dataDir);
|
|
590
|
+
for (const file of files) {
|
|
591
|
+
if (!file.endsWith(".json")) continue;
|
|
592
|
+
const key = `/${file.replace(/\.json$/, "").replace(/%2F/gi, "/")}`;
|
|
593
|
+
try {
|
|
594
|
+
store[key] = JSON.parse(await platform.fs.readTextFile(platform.path.join(this.dataDir, file)));
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
597
|
+
} catch {}
|
|
598
|
+
return store;
|
|
599
|
+
}
|
|
600
|
+
onMount(afs) {
|
|
601
|
+
this.destroy();
|
|
602
|
+
this.afsRoot = afs;
|
|
603
|
+
this.initTriggers().catch(() => {});
|
|
604
|
+
if (afs.subscribe) this.externalScriptSubs = [afs.subscribe({ type: SCRIPT_EVENT_TYPES.REGISTERED }, (evt) => {
|
|
605
|
+
if (evt.data?.runtime !== "ash") return;
|
|
606
|
+
if (this.isFromProgramNamespace(evt.path)) return;
|
|
607
|
+
if (this.programAfses.has(afs)) return;
|
|
608
|
+
this.trackPendingTrigger(this.registerExternalScript(evt.path).catch(() => {}));
|
|
609
|
+
}), afs.subscribe({ type: SCRIPT_EVENT_TYPES.UNREGISTERED }, (evt) => {
|
|
610
|
+
if (evt.data?.runtime !== "ash") return;
|
|
611
|
+
if (this.isFromProgramNamespace(evt.path)) return;
|
|
612
|
+
if (this.programAfses.has(afs)) return;
|
|
613
|
+
this.unregisterExternalScript(evt.path);
|
|
614
|
+
})];
|
|
615
|
+
}
|
|
616
|
+
/** Check if a global event path belongs to a registered program namespace. */
|
|
617
|
+
isFromProgramNamespace(path) {
|
|
618
|
+
if (path.startsWith("/blocklets/")) return true;
|
|
619
|
+
for (const ns of this.programNamespaces) if (path.startsWith(`${ns}/`)) return true;
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
async registerExternalScript(path) {
|
|
623
|
+
const afs = this.afsRoot;
|
|
624
|
+
if (!afs?.read) return;
|
|
625
|
+
for (const ns of this.programNamespaces) if (this.triggerAfsOverrides.has(`${ns}:${path}`)) return;
|
|
626
|
+
for (const key of this.triggerSubs.keys()) if (key.endsWith(`:${path}`)) return;
|
|
627
|
+
try {
|
|
628
|
+
const file = await afs.read(path, {});
|
|
629
|
+
const source = String(file.data?.content ?? "");
|
|
630
|
+
if (!source.trim()) return;
|
|
631
|
+
this.externalScripts.set(path, source);
|
|
632
|
+
this.registerTriggers(path, source);
|
|
633
|
+
} catch {}
|
|
634
|
+
}
|
|
635
|
+
unregisterExternalScript(path) {
|
|
636
|
+
this.externalScripts.delete(path);
|
|
637
|
+
this.unregisterTriggers(path);
|
|
638
|
+
}
|
|
639
|
+
/** Wait for all pending trigger executions to complete. Useful in tests. */
|
|
640
|
+
async flushTriggers() {
|
|
641
|
+
while (this.pendingTriggers.size > 0) await Promise.allSettled([...this.pendingTriggers]);
|
|
642
|
+
}
|
|
643
|
+
trackPendingTrigger(promise) {
|
|
644
|
+
this.pendingTriggers.add(promise);
|
|
645
|
+
promise.finally(() => {
|
|
646
|
+
this.pendingTriggers.delete(promise);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/** Stop all cron jobs and unsubscribe all event triggers. Call on provider teardown. */
|
|
650
|
+
destroy() {
|
|
651
|
+
const allNames = new Set([...this.triggerSubs.keys(), ...this.cronJobs.keys()]);
|
|
652
|
+
for (const name of allNames) this.unregisterTriggers(name);
|
|
653
|
+
for (const unsub of this.externalScriptSubs) unsub();
|
|
654
|
+
this.externalScriptSubs = [];
|
|
655
|
+
this.externalScripts.clear();
|
|
656
|
+
this.triggerAfsOverrides.clear();
|
|
657
|
+
this.programTriggerKeys.clear();
|
|
658
|
+
this.programNamespaces.clear();
|
|
659
|
+
}
|
|
660
|
+
async initTriggers() {
|
|
661
|
+
await this.ensureDirs();
|
|
662
|
+
for (const name of await this.getAllScriptNames()) {
|
|
663
|
+
const script = await this.getScript(name);
|
|
664
|
+
if (script) this.registerTriggers(name, script.source);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Register triggers for all .ash scripts in a program's /program/scripts/ directory.
|
|
669
|
+
* Uses the program's runtime AFS for both event subscription and action execution,
|
|
670
|
+
* ensuring triggered jobs operate in the correct program namespace (e.g., accessing
|
|
671
|
+
* the program's own telegram/slack providers instead of the host's).
|
|
672
|
+
*/
|
|
673
|
+
/**
|
|
674
|
+
* Register triggers for all .ash scripts in a program's script directories.
|
|
675
|
+
* @param runtimeAFS - The program's runtime AFS instance
|
|
676
|
+
* @param namespace - Optional namespace prefix to avoid key collisions when
|
|
677
|
+
* multiple blocklets share the same ASH provider instance.
|
|
678
|
+
* e.g. "/blocklets/telegram-assistant" → keys become
|
|
679
|
+
* "/blocklets/telegram-assistant:/program/scripts/foo.ash"
|
|
680
|
+
*/
|
|
681
|
+
async registerProgramTriggers(runtimeAFS, namespace) {
|
|
682
|
+
if (!runtimeAFS.list || !runtimeAFS.read) return;
|
|
683
|
+
if (namespace) this.programNamespaces.add(namespace);
|
|
684
|
+
this.programAfses.add(runtimeAFS);
|
|
685
|
+
const keyPrefix = namespace ? `${namespace}:` : "";
|
|
686
|
+
const programKey = namespace ?? "__default__";
|
|
687
|
+
const subKey = `${keyPrefix}__program__`;
|
|
688
|
+
this.unregisterProgramTriggers(programKey);
|
|
689
|
+
this.unregisterProgramSubscription(subKey);
|
|
690
|
+
let registeredCount = 0;
|
|
691
|
+
for (const dir of [
|
|
692
|
+
"/packages/scripts",
|
|
693
|
+
"/instance/scripts",
|
|
694
|
+
"/instance"
|
|
695
|
+
]) try {
|
|
696
|
+
const scriptsList = await runtimeAFS.list(dir, {});
|
|
697
|
+
for (const entry of scriptsList.data) {
|
|
698
|
+
if (!entry.path.endsWith(".ash")) continue;
|
|
699
|
+
const scriptPath = entry.path.startsWith("/") ? entry.path : `${dir}/${entry.path}`;
|
|
700
|
+
try {
|
|
701
|
+
const readResult = await runtimeAFS.read(scriptPath, {});
|
|
702
|
+
const source = String(readResult.data?.content ?? "");
|
|
703
|
+
if (!source.trim()) continue;
|
|
704
|
+
const result = compileSource(source);
|
|
705
|
+
if (!result.program) continue;
|
|
706
|
+
if (!result.program.units.some((u) => u.kind === "job" && u.trigger)) continue;
|
|
707
|
+
if (this.triggerSubs.has(scriptPath)) {
|
|
708
|
+
this.unregisterTriggers(scriptPath);
|
|
709
|
+
this.externalScripts.delete(scriptPath);
|
|
710
|
+
}
|
|
711
|
+
this.registerProgramScriptTrigger(programKey, keyPrefix + scriptPath, source, runtimeAFS);
|
|
712
|
+
registeredCount++;
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
} catch {}
|
|
716
|
+
log.debug(`[ASH] registerProgramTriggers ns=${namespace ?? "(none)"} registered=${registeredCount}`);
|
|
717
|
+
if (runtimeAFS.subscribe) {
|
|
718
|
+
const unsub = runtimeAFS.subscribe({ type: SCRIPT_EVENT_TYPES.REGISTERED }, (evt) => {
|
|
719
|
+
if (evt.data?.runtime !== "ash") return;
|
|
720
|
+
const p = (async () => {
|
|
721
|
+
try {
|
|
722
|
+
const readResult = await runtimeAFS.read(evt.path, {});
|
|
723
|
+
const source = String(readResult.data?.content ?? "");
|
|
724
|
+
if (!source.trim()) return;
|
|
725
|
+
this.registerProgramScriptTrigger(programKey, keyPrefix + evt.path, source, runtimeAFS);
|
|
726
|
+
} catch {}
|
|
727
|
+
})();
|
|
728
|
+
this.trackPendingTrigger(p);
|
|
729
|
+
});
|
|
730
|
+
this.triggerSubs.set(subKey, [unsub]);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
registerProgramScriptTrigger(programKey, scriptName, source, runtimeAFS) {
|
|
734
|
+
this.registerTriggers(scriptName, source, runtimeAFS);
|
|
735
|
+
const keys = this.programTriggerKeys.get(programKey) ?? /* @__PURE__ */ new Set();
|
|
736
|
+
keys.add(scriptName);
|
|
737
|
+
this.programTriggerKeys.set(programKey, keys);
|
|
738
|
+
}
|
|
739
|
+
unregisterProgramTriggers(programKey) {
|
|
740
|
+
const keys = this.programTriggerKeys.get(programKey);
|
|
741
|
+
if (!keys) return;
|
|
742
|
+
for (const scriptName of keys) this.unregisterTriggers(scriptName);
|
|
743
|
+
this.programTriggerKeys.delete(programKey);
|
|
744
|
+
}
|
|
745
|
+
unregisterProgramSubscription(subKey) {
|
|
746
|
+
const existing = this.triggerSubs.get(subKey);
|
|
747
|
+
if (!existing) return;
|
|
748
|
+
for (const unsub of existing) unsub();
|
|
749
|
+
this.triggerSubs.delete(subKey);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Register EventBus subscriptions and cron schedules for all triggers in a script.
|
|
753
|
+
* Replaces any previous subscriptions/schedules for the same script name.
|
|
754
|
+
*
|
|
755
|
+
* When `afsOverride` is provided (program-scoped triggers), events are subscribed
|
|
756
|
+
* on the program AFS and the override is stored for use by `executeTriggerJob`.
|
|
757
|
+
* When re-registered without an explicit override (e.g., via script:registered),
|
|
758
|
+
* any existing program-scoped override is preserved.
|
|
759
|
+
*/
|
|
760
|
+
registerTriggers(scriptName, source, afsOverride) {
|
|
761
|
+
const effectiveOverride = afsOverride ?? this.triggerAfsOverrides.get(scriptName);
|
|
762
|
+
this.unregisterTriggers(scriptName);
|
|
763
|
+
if (effectiveOverride) this.triggerAfsOverrides.set(scriptName, effectiveOverride);
|
|
764
|
+
const result = compileSource(source);
|
|
765
|
+
if (!result.program) return;
|
|
766
|
+
const afs = effectiveOverride ?? this.afsRoot;
|
|
767
|
+
const unsubs = [];
|
|
768
|
+
const crons = [];
|
|
769
|
+
for (const unit of result.program.units) {
|
|
770
|
+
if (unit.kind !== "job" || !unit.trigger) continue;
|
|
771
|
+
const jobName = unit.name;
|
|
772
|
+
const trigger = unit.trigger;
|
|
773
|
+
if (trigger.kind === "event") {
|
|
774
|
+
if (!afs?.subscribe) continue;
|
|
775
|
+
const filter = {
|
|
776
|
+
path: trigger.path,
|
|
777
|
+
type: trigger.event ? `*:${trigger.event}` : void 0
|
|
778
|
+
};
|
|
779
|
+
const unsub = afs.subscribe(filter, (evt) => {
|
|
780
|
+
const p = this.executeTriggerJob(scriptName, jobName, evt);
|
|
781
|
+
this.trackPendingTrigger(p);
|
|
782
|
+
});
|
|
783
|
+
unsubs.push(unsub);
|
|
784
|
+
} else if (trigger.kind === "cron") try {
|
|
785
|
+
new CronPattern(trigger.expression);
|
|
786
|
+
const cronKey = `${scriptName}:${jobName}`;
|
|
787
|
+
this.cronRegistry.set(cronKey, {
|
|
788
|
+
expression: trigger.expression,
|
|
789
|
+
scriptName,
|
|
790
|
+
jobName,
|
|
791
|
+
lastFiredMinute: 0
|
|
792
|
+
});
|
|
793
|
+
const cron = new Cron(trigger.expression, { protect: true }, () => {
|
|
794
|
+
const reg = this.cronRegistry.get(cronKey);
|
|
795
|
+
if (reg) reg.lastFiredMinute = Math.floor(Date.now() / 6e4);
|
|
796
|
+
const syntheticEvent = {
|
|
797
|
+
type: "cron",
|
|
798
|
+
path: `/cron/${scriptName}/${jobName}`,
|
|
799
|
+
source: "ash-cron",
|
|
800
|
+
timestamp: Date.now()
|
|
801
|
+
};
|
|
802
|
+
const p = this.executeTriggerJob(scriptName, jobName, syntheticEvent);
|
|
803
|
+
this.trackPendingTrigger(p);
|
|
804
|
+
});
|
|
805
|
+
crons.push(cron);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
log.error(`[ASH cron] Invalid cron expression "${trigger.expression}" in ${scriptName}/${jobName}:`, err);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (unsubs.length > 0) this.triggerSubs.set(scriptName, unsubs);
|
|
811
|
+
if (crons.length > 0) this.cronJobs.set(scriptName, crons);
|
|
812
|
+
}
|
|
813
|
+
/** Unsubscribe all event triggers and stop all cron jobs for a given script. */
|
|
814
|
+
unregisterTriggers(scriptName) {
|
|
815
|
+
const unsubs = this.triggerSubs.get(scriptName);
|
|
816
|
+
if (unsubs) {
|
|
817
|
+
for (const unsub of unsubs) unsub();
|
|
818
|
+
this.triggerSubs.delete(scriptName);
|
|
819
|
+
}
|
|
820
|
+
const crons = this.cronJobs.get(scriptName);
|
|
821
|
+
if (crons) {
|
|
822
|
+
for (const cron of crons) cron.stop();
|
|
823
|
+
this.cronJobs.delete(scriptName);
|
|
824
|
+
}
|
|
825
|
+
for (const key of this.cronRegistry.keys()) if (key.startsWith(`${scriptName}:`)) this.cronRegistry.delete(key);
|
|
826
|
+
this.triggerAfsOverrides.delete(scriptName);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Tick all registered cron triggers — called by external alarm (e.g., DO alarm).
|
|
830
|
+
* Checks each cron expression against the current minute and fires matching jobs.
|
|
831
|
+
* Uses lastFiredMinute to prevent double-firing (croner timer may also fire).
|
|
832
|
+
*/
|
|
833
|
+
tickCronTriggers() {
|
|
834
|
+
const now = Date.now();
|
|
835
|
+
const currentMinute = Math.floor(now / 6e4);
|
|
836
|
+
for (const [, entry] of this.cronRegistry) {
|
|
837
|
+
if (entry.lastFiredMinute >= currentMinute) continue;
|
|
838
|
+
try {
|
|
839
|
+
if (!new Cron(entry.expression).match(new Date(now))) continue;
|
|
840
|
+
} catch {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
entry.lastFiredMinute = currentMinute;
|
|
844
|
+
const syntheticEvent = {
|
|
845
|
+
type: "cron",
|
|
846
|
+
path: `/cron/${entry.scriptName}/${entry.jobName}`,
|
|
847
|
+
source: "ash-cron-tick",
|
|
848
|
+
timestamp: now
|
|
849
|
+
};
|
|
850
|
+
const p = this.executeTriggerJob(entry.scriptName, entry.jobName, syntheticEvent);
|
|
851
|
+
this.trackPendingTrigger(p);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Execute a single triggered job reactively in response to an AFS event.
|
|
856
|
+
* Re-compiles the script, builds a bridged world context, executes the
|
|
857
|
+
* target job with event data as the initial stream, and writes results
|
|
858
|
+
* back to AFS.
|
|
859
|
+
*/
|
|
860
|
+
async executeTriggerJob(scriptName, jobName, event) {
|
|
861
|
+
try {
|
|
862
|
+
let source;
|
|
863
|
+
const script = await this.getScript(scriptName);
|
|
864
|
+
if (script) source = script.source;
|
|
865
|
+
else if (this.externalScripts.has(scriptName)) source = this.externalScripts.get(scriptName);
|
|
866
|
+
else {
|
|
867
|
+
const readAFS = this.triggerAfsOverrides.get(scriptName) ?? this.afsRoot;
|
|
868
|
+
const colonIdx = scriptName.indexOf(":");
|
|
869
|
+
const readPath = colonIdx >= 0 ? scriptName.slice(colonIdx + 1) : scriptName;
|
|
870
|
+
if (readAFS?.read) try {
|
|
871
|
+
const file = await readAFS.read(readPath, {});
|
|
872
|
+
source = String(file.data?.content ?? "");
|
|
873
|
+
} catch {}
|
|
874
|
+
}
|
|
875
|
+
if (!source) return;
|
|
876
|
+
const result = compileSource(source);
|
|
877
|
+
if (!result.program) return;
|
|
878
|
+
const job = result.program.units.find((u) => u.kind === "job" && u.name === jobName);
|
|
879
|
+
if (!job || job.kind !== "job") return;
|
|
880
|
+
const afsOverride = this.triggerAfsOverrides.get(scriptName);
|
|
881
|
+
const effectiveAFS = afsOverride ?? this.afsRoot;
|
|
882
|
+
const { world, written } = await this.createAsyncWorld(afsOverride);
|
|
883
|
+
const ctx = {
|
|
884
|
+
world: effectiveAFS ? {
|
|
885
|
+
read: (path, query) => readAfsRecords(effectiveAFS, world, path, query),
|
|
886
|
+
write: (path, data) => {
|
|
887
|
+
world.write(path, data);
|
|
888
|
+
},
|
|
889
|
+
publish: (topic, data) => {
|
|
890
|
+
world.publish(topic, data);
|
|
891
|
+
},
|
|
892
|
+
exec: world.exec && afsOverride ? async (path, input, params) => {
|
|
893
|
+
const augmentedInput = input.map((item) => typeof item === "object" && item !== null ? {
|
|
894
|
+
...item,
|
|
895
|
+
_scope_afs: afsOverride
|
|
896
|
+
} : item);
|
|
897
|
+
return world.exec(path, augmentedInput, params);
|
|
898
|
+
} : world.exec
|
|
899
|
+
} : world,
|
|
900
|
+
caps: new Set(["*"]),
|
|
901
|
+
logger: { log() {} },
|
|
902
|
+
output: { output() {} }
|
|
903
|
+
};
|
|
904
|
+
const initialStream = [{ ...event }];
|
|
905
|
+
const jobResult = await job.execute(ctx, initialStream);
|
|
906
|
+
if (jobResult.status !== "ok") log.error(`[ASH trigger] ${scriptName}/${jobName} status=${jobResult.status}`, jobResult.errors);
|
|
907
|
+
if (effectiveAFS?.write) for (const [path, data] of Object.entries(written)) if (path.endsWith(".log")) {
|
|
908
|
+
const content = `${data.map((item) => JSON.stringify(item)).join("\n")}\n`;
|
|
909
|
+
await effectiveAFS.write(path, { content }, { mode: "append" });
|
|
910
|
+
} else await effectiveAFS.write(path, { content: JSON.stringify(data) });
|
|
911
|
+
} catch (err) {
|
|
912
|
+
log.error(`[ASH trigger] ${scriptName}/${jobName} threw:`, err);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async listRoot(_ctx) {
|
|
916
|
+
return { data: [{
|
|
917
|
+
id: "/scripts",
|
|
918
|
+
path: "/scripts",
|
|
919
|
+
summary: "Stored ASH scripts",
|
|
920
|
+
meta: {
|
|
921
|
+
kind: "ash:directory",
|
|
922
|
+
type: "directory",
|
|
923
|
+
childrenCount: await this.getScriptCount()
|
|
924
|
+
}
|
|
925
|
+
}, {
|
|
926
|
+
id: "/cookbook",
|
|
927
|
+
path: "/cookbook",
|
|
928
|
+
summary: "ASH best practices and quick reference",
|
|
929
|
+
meta: {
|
|
930
|
+
kind: "ash:directory",
|
|
931
|
+
type: "directory",
|
|
932
|
+
childrenCount: 2
|
|
933
|
+
}
|
|
934
|
+
}] };
|
|
935
|
+
}
|
|
936
|
+
async listScripts(_ctx) {
|
|
937
|
+
const entries = [];
|
|
938
|
+
for (const name of await this.getAllScriptNames()) {
|
|
939
|
+
const script = await this.getScript(name);
|
|
940
|
+
if (!script) continue;
|
|
941
|
+
entries.push(this.buildScriptEntry(name, script));
|
|
942
|
+
}
|
|
943
|
+
return { data: entries };
|
|
944
|
+
}
|
|
945
|
+
async listScript(ctx) {
|
|
946
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
947
|
+
if (!await this.hasScript(name)) throw new AFSNotFoundError(ctx.path);
|
|
948
|
+
return { data: [] };
|
|
949
|
+
}
|
|
950
|
+
async listRootActions(_ctx) {
|
|
951
|
+
return { data: [{
|
|
952
|
+
id: "run",
|
|
953
|
+
path: "/.actions/run",
|
|
954
|
+
summary: "Execute inline ASH code",
|
|
955
|
+
meta: {
|
|
956
|
+
kind: "afs:executable",
|
|
957
|
+
kinds: ["afs:executable", "afs:node"],
|
|
958
|
+
name: "run",
|
|
959
|
+
description: "Execute inline ASH pipeline code",
|
|
960
|
+
inputSchema: {
|
|
961
|
+
type: "object",
|
|
962
|
+
properties: {
|
|
963
|
+
source: {
|
|
964
|
+
type: "string",
|
|
965
|
+
description: "ASH pipeline source code. Read /.meta/.ash-reference for syntax."
|
|
966
|
+
},
|
|
967
|
+
caps: {
|
|
968
|
+
type: "array",
|
|
969
|
+
items: { type: "string" },
|
|
970
|
+
description: "Capability paths. Required for write-back to AFS (default: no write-back without caps)"
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
required: ["source"]
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}, {
|
|
977
|
+
id: "validate",
|
|
978
|
+
path: "/.actions/validate",
|
|
979
|
+
summary: "Validate ASH source code",
|
|
980
|
+
meta: {
|
|
981
|
+
kind: "afs:executable",
|
|
982
|
+
kinds: ["afs:executable", "afs:node"],
|
|
983
|
+
name: "validate",
|
|
984
|
+
description: "Compile and type-check ASH source, return diagnostics",
|
|
985
|
+
inputSchema: {
|
|
986
|
+
type: "object",
|
|
987
|
+
properties: { source: {
|
|
988
|
+
type: "string",
|
|
989
|
+
description: "ASH pipeline source code to validate. Read /.meta/.ash-reference for syntax."
|
|
990
|
+
} },
|
|
991
|
+
required: ["source"]
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}] };
|
|
995
|
+
}
|
|
996
|
+
async listScriptActions(ctx) {
|
|
997
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
998
|
+
const script = await this.getScript(name);
|
|
999
|
+
if (!script) throw new AFSNotFoundError(ctx.path);
|
|
1000
|
+
const inputSchema = this.buildScriptExecInputSchema(script.source);
|
|
1001
|
+
return { data: [{
|
|
1002
|
+
id: "exec",
|
|
1003
|
+
path: joinURL(joinURL("/scripts", ctx.params.scriptName, ".actions"), "exec"),
|
|
1004
|
+
summary: `Execute script ${name}`,
|
|
1005
|
+
meta: {
|
|
1006
|
+
kind: "afs:executable",
|
|
1007
|
+
kinds: ["afs:executable", "afs:node"],
|
|
1008
|
+
name: "exec",
|
|
1009
|
+
description: `Execute the ${name} ASH script`,
|
|
1010
|
+
inputSchema
|
|
1011
|
+
}
|
|
1012
|
+
}] };
|
|
1013
|
+
}
|
|
1014
|
+
async readRoot(_ctx) {
|
|
1015
|
+
return {
|
|
1016
|
+
id: "/",
|
|
1017
|
+
path: "/",
|
|
1018
|
+
meta: {
|
|
1019
|
+
type: "directory",
|
|
1020
|
+
childrenCount: 2
|
|
1021
|
+
},
|
|
1022
|
+
actions: [{
|
|
1023
|
+
name: "run",
|
|
1024
|
+
description: "Execute inline ASH code"
|
|
1025
|
+
}, {
|
|
1026
|
+
name: "validate",
|
|
1027
|
+
description: "Validate ASH source code"
|
|
1028
|
+
}]
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async readScriptsDir(_ctx) {
|
|
1032
|
+
return {
|
|
1033
|
+
id: "/scripts",
|
|
1034
|
+
path: "/scripts",
|
|
1035
|
+
summary: "Stored ASH scripts",
|
|
1036
|
+
meta: {
|
|
1037
|
+
kind: "ash:directory",
|
|
1038
|
+
type: "directory",
|
|
1039
|
+
childrenCount: await this.getScriptCount()
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
async readScript(ctx) {
|
|
1044
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1045
|
+
const script = await this.getScript(name);
|
|
1046
|
+
if (!script) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1047
|
+
return this.buildScriptEntry(name, script);
|
|
1048
|
+
}
|
|
1049
|
+
static COOKBOOK_FILES = ["patterns.md", "quickref.md"];
|
|
1050
|
+
async listCookbook(_ctx) {
|
|
1051
|
+
return { data: AFSAsh.COOKBOOK_FILES.map((file) => ({
|
|
1052
|
+
id: joinURL("/cookbook", file),
|
|
1053
|
+
path: joinURL("/cookbook", file),
|
|
1054
|
+
summary: file === "patterns.md" ? "agent-run patterns and pitfalls" : "Quick reference card",
|
|
1055
|
+
meta: { kind: "ash:documentation" }
|
|
1056
|
+
})) };
|
|
1057
|
+
}
|
|
1058
|
+
async readCookbookDir(_ctx) {
|
|
1059
|
+
return {
|
|
1060
|
+
id: "/cookbook",
|
|
1061
|
+
path: "/cookbook",
|
|
1062
|
+
meta: {
|
|
1063
|
+
kind: "ash:directory",
|
|
1064
|
+
type: "directory",
|
|
1065
|
+
childrenCount: AFSAsh.COOKBOOK_FILES.length
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async listCookbookFile(_ctx) {
|
|
1070
|
+
return { data: [] };
|
|
1071
|
+
}
|
|
1072
|
+
async readCookbook(ctx) {
|
|
1073
|
+
const file = ctx.params.file;
|
|
1074
|
+
if (!AFSAsh.COOKBOOK_FILES.includes(file)) throw new AFSNotFoundError(ctx.path);
|
|
1075
|
+
return {
|
|
1076
|
+
id: joinURL("/cookbook", file),
|
|
1077
|
+
path: joinURL("/cookbook", file),
|
|
1078
|
+
content: await readDoc(joinURL("cookbook", file)),
|
|
1079
|
+
meta: { kind: "ash:documentation" }
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async readAgentMd(_ctx) {
|
|
1083
|
+
return {
|
|
1084
|
+
id: "/.meta/agent.md",
|
|
1085
|
+
path: "/.meta/agent.md",
|
|
1086
|
+
content: await readDoc("agent.md"),
|
|
1087
|
+
meta: {
|
|
1088
|
+
kind: "ash:documentation",
|
|
1089
|
+
description: "ASH agent usage guide"
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
async readAshReference(_ctx) {
|
|
1094
|
+
return {
|
|
1095
|
+
id: "/.meta/.ash-reference",
|
|
1096
|
+
path: "/.meta/.ash-reference",
|
|
1097
|
+
content: ASH_REFERENCE,
|
|
1098
|
+
meta: {
|
|
1099
|
+
kind: "ash:reference",
|
|
1100
|
+
description: "ASH language reference for writing valid ASH pipelines"
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async readCapabilities(_ctx) {
|
|
1105
|
+
return {
|
|
1106
|
+
id: "/.meta/.capabilities",
|
|
1107
|
+
path: "/.meta/.capabilities",
|
|
1108
|
+
content: {
|
|
1109
|
+
schemaVersion: 1,
|
|
1110
|
+
provider: this.name,
|
|
1111
|
+
description: `${this.description}. Read /.meta/.ash-reference for ASH syntax.`,
|
|
1112
|
+
tools: [],
|
|
1113
|
+
operations: this.getOperationsDeclaration(),
|
|
1114
|
+
actions: [{
|
|
1115
|
+
description: "Root-level ASH actions",
|
|
1116
|
+
catalog: [{
|
|
1117
|
+
name: "run",
|
|
1118
|
+
description: "Execute inline ASH code"
|
|
1119
|
+
}, {
|
|
1120
|
+
name: "validate",
|
|
1121
|
+
description: "Validate ASH source code"
|
|
1122
|
+
}],
|
|
1123
|
+
discovery: { pathTemplate: "/.actions" }
|
|
1124
|
+
}, {
|
|
1125
|
+
kind: "ash:script",
|
|
1126
|
+
description: "Script-level actions",
|
|
1127
|
+
catalog: [{
|
|
1128
|
+
name: "exec",
|
|
1129
|
+
description: "Execute the script"
|
|
1130
|
+
}],
|
|
1131
|
+
discovery: { pathTemplate: "/scripts/:scriptName/.actions" }
|
|
1132
|
+
}],
|
|
1133
|
+
resources: { caps: [
|
|
1134
|
+
{
|
|
1135
|
+
op: "read",
|
|
1136
|
+
path: "/*",
|
|
1137
|
+
description: "Read scripts and results"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
op: "write",
|
|
1141
|
+
path: "/scripts/*",
|
|
1142
|
+
description: "Write ASH script files"
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
op: "exec",
|
|
1146
|
+
path: "/.actions/*",
|
|
1147
|
+
description: "Execute root actions (run, validate)"
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
op: "exec",
|
|
1151
|
+
path: "/scripts/*/.actions/*",
|
|
1152
|
+
description: "Execute a script"
|
|
1153
|
+
}
|
|
1154
|
+
] }
|
|
1155
|
+
},
|
|
1156
|
+
meta: {
|
|
1157
|
+
kind: "afs:capabilities",
|
|
1158
|
+
description: "ASH provider capabilities manifest",
|
|
1159
|
+
operations: [
|
|
1160
|
+
"list",
|
|
1161
|
+
"read",
|
|
1162
|
+
"write",
|
|
1163
|
+
"delete",
|
|
1164
|
+
"exec",
|
|
1165
|
+
"stat",
|
|
1166
|
+
"explain",
|
|
1167
|
+
"search"
|
|
1168
|
+
]
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async readRootMeta(_ctx) {
|
|
1173
|
+
return {
|
|
1174
|
+
id: "/.meta",
|
|
1175
|
+
path: "/.meta",
|
|
1176
|
+
content: {
|
|
1177
|
+
type: "directory",
|
|
1178
|
+
childrenCount: 2
|
|
1179
|
+
},
|
|
1180
|
+
meta: {
|
|
1181
|
+
type: "directory",
|
|
1182
|
+
childrenCount: 2
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
async readCookbookMeta(_ctx) {
|
|
1187
|
+
return {
|
|
1188
|
+
id: "/cookbook/.meta",
|
|
1189
|
+
path: "/cookbook/.meta",
|
|
1190
|
+
content: {
|
|
1191
|
+
kind: "ash:directory",
|
|
1192
|
+
type: "directory",
|
|
1193
|
+
childrenCount: AFSAsh.COOKBOOK_FILES.length
|
|
1194
|
+
},
|
|
1195
|
+
meta: {
|
|
1196
|
+
kind: "ash:directory",
|
|
1197
|
+
type: "directory",
|
|
1198
|
+
childrenCount: AFSAsh.COOKBOOK_FILES.length
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async readCookbookFileMeta(ctx) {
|
|
1203
|
+
return {
|
|
1204
|
+
id: joinURL("/cookbook", ctx.params.file, ".meta"),
|
|
1205
|
+
path: joinURL("/cookbook", ctx.params.file, ".meta"),
|
|
1206
|
+
content: { kind: "ash:documentation" },
|
|
1207
|
+
meta: { kind: "ash:documentation" }
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
async readScriptsMeta(_ctx) {
|
|
1211
|
+
return {
|
|
1212
|
+
id: "/scripts/.meta",
|
|
1213
|
+
path: "/scripts/.meta",
|
|
1214
|
+
content: {
|
|
1215
|
+
type: "directory",
|
|
1216
|
+
childrenCount: await this.getScriptCount()
|
|
1217
|
+
},
|
|
1218
|
+
meta: {
|
|
1219
|
+
type: "directory",
|
|
1220
|
+
childrenCount: await this.getScriptCount()
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
async readScriptMeta(ctx) {
|
|
1225
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1226
|
+
const script = await this.getScript(name);
|
|
1227
|
+
if (!script) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1228
|
+
const entry = this.buildScriptEntry(name, script);
|
|
1229
|
+
return {
|
|
1230
|
+
id: joinURL("/scripts", ctx.params.scriptName, ".meta"),
|
|
1231
|
+
path: joinURL("/scripts", ctx.params.scriptName, ".meta"),
|
|
1232
|
+
content: entry.meta ?? {},
|
|
1233
|
+
meta: entry.meta ?? {}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async writeScript(ctx, entry) {
|
|
1237
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1238
|
+
const now = /* @__PURE__ */ new Date();
|
|
1239
|
+
const rawContent = typeof entry === "string" ? entry : entry.content;
|
|
1240
|
+
let existed = false;
|
|
1241
|
+
const stored = await this.updateScript(name, async (existing) => {
|
|
1242
|
+
existed = existing !== void 0;
|
|
1243
|
+
if (rawContent === void 0 && existing) return {
|
|
1244
|
+
source: existing.source,
|
|
1245
|
+
createdAt: existing.createdAt,
|
|
1246
|
+
updatedAt: now
|
|
1247
|
+
};
|
|
1248
|
+
return {
|
|
1249
|
+
source: typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent) ?? "",
|
|
1250
|
+
createdAt: existing?.createdAt ?? now,
|
|
1251
|
+
updatedAt: now
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
if (!stored) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1255
|
+
this.registerTriggers(name, stored.source);
|
|
1256
|
+
return {
|
|
1257
|
+
data: this.buildScriptEntry(name, stored),
|
|
1258
|
+
message: existed ? "Script updated" : "Script created"
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async deleteScript(ctx) {
|
|
1262
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1263
|
+
let existed = false;
|
|
1264
|
+
await this.updateScript(name, async (existing) => {
|
|
1265
|
+
existed = existing !== void 0 || await this.hasScript(name);
|
|
1266
|
+
});
|
|
1267
|
+
if (!existed) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1268
|
+
this.unregisterTriggers(name);
|
|
1269
|
+
return { message: `Script '${name}' deleted` };
|
|
1270
|
+
}
|
|
1271
|
+
async deleteCatchAll(ctx) {
|
|
1272
|
+
throw new AFSNotFoundError(ctx.path);
|
|
1273
|
+
}
|
|
1274
|
+
async execScript(ctx, args) {
|
|
1275
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1276
|
+
const script = await this.getScript(name);
|
|
1277
|
+
if (!script) return {
|
|
1278
|
+
success: false,
|
|
1279
|
+
data: { error: `Script not found: ${name}` }
|
|
1280
|
+
};
|
|
1281
|
+
return this.runAsh(script.source, args);
|
|
1282
|
+
}
|
|
1283
|
+
async execScriptDirect(ctx, args) {
|
|
1284
|
+
return this.execScript(ctx, args);
|
|
1285
|
+
}
|
|
1286
|
+
async execRootAction(ctx, args) {
|
|
1287
|
+
const actionName = ctx.params.action;
|
|
1288
|
+
if (actionName === "run") {
|
|
1289
|
+
const source = args.source;
|
|
1290
|
+
if (typeof source !== "string") throw new Error("action 'run' requires 'source' parameter as string");
|
|
1291
|
+
const jobName = args.job;
|
|
1292
|
+
if (jobName) {
|
|
1293
|
+
const runtimeAFS$1 = args._scope_afs;
|
|
1294
|
+
return this.runJob(source, jobName, args, { afsOverride: runtimeAFS$1 });
|
|
1295
|
+
}
|
|
1296
|
+
const hasCaps = Array.isArray(args.caps) && args.caps.length > 0;
|
|
1297
|
+
const runtimeAFS = args._scope_afs;
|
|
1298
|
+
const returnWrittenData = args._return_written_data === true;
|
|
1299
|
+
const skipWritePrefix = typeof args._skip_write_prefix === "string" ? args._skip_write_prefix : void 0;
|
|
1300
|
+
const cleanArgs = { ...args };
|
|
1301
|
+
delete cleanArgs._scope_afs;
|
|
1302
|
+
delete cleanArgs._return_written_data;
|
|
1303
|
+
delete cleanArgs._skip_write_prefix;
|
|
1304
|
+
return this.runAsh(source, cleanArgs, {
|
|
1305
|
+
allowWriteBack: hasCaps,
|
|
1306
|
+
afsOverride: runtimeAFS,
|
|
1307
|
+
returnWrittenData,
|
|
1308
|
+
skipWritePrefix
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (actionName === "validate") {
|
|
1312
|
+
const source = args.source;
|
|
1313
|
+
if (typeof source !== "string") throw new Error("action 'validate' requires 'source' parameter as string");
|
|
1314
|
+
const result = compileSource(source);
|
|
1315
|
+
const errors = result.diagnostics.filter((d) => d.severity !== "warning");
|
|
1316
|
+
const warnings = result.diagnostics.filter((d) => d.severity === "warning");
|
|
1317
|
+
return {
|
|
1318
|
+
success: true,
|
|
1319
|
+
data: {
|
|
1320
|
+
valid: errors.length === 0,
|
|
1321
|
+
errors,
|
|
1322
|
+
warnings,
|
|
1323
|
+
jobCount: result.program?.jobs.length ?? 0,
|
|
1324
|
+
sourceHash: result.sourceHash
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (actionName === "agent-run") {
|
|
1329
|
+
if (!ashAgentRunDeprecationLogged) {
|
|
1330
|
+
log.warn("[ash] DEPRECATED: /ash/.actions/agent-run — use /dev/agent/.actions/run instead. This alias will be removed in a future version.");
|
|
1331
|
+
ashAgentRunDeprecationLogged = true;
|
|
1332
|
+
}
|
|
1333
|
+
const afs = args._scope_afs ?? this.afsRoot;
|
|
1334
|
+
if (!afs?.exec) return {
|
|
1335
|
+
success: false,
|
|
1336
|
+
error: {
|
|
1337
|
+
code: "AGENT_RUN_ALIAS_NO_AFS",
|
|
1338
|
+
message: "AFS root not available"
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
return afs.exec("/dev/agent/.actions/run", args, {});
|
|
1342
|
+
}
|
|
1343
|
+
if (actionName === "register-triggers") {
|
|
1344
|
+
const runtimeAFS = args._scope_afs;
|
|
1345
|
+
const namespace = typeof args.namespace === "string" ? args.namespace : void 0;
|
|
1346
|
+
if (runtimeAFS) await this.registerProgramTriggers(runtimeAFS, namespace);
|
|
1347
|
+
return {
|
|
1348
|
+
success: true,
|
|
1349
|
+
data: { registered: !!runtimeAFS }
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (actionName === "tick-crons") {
|
|
1353
|
+
this.tickCronTriggers();
|
|
1354
|
+
return {
|
|
1355
|
+
success: true,
|
|
1356
|
+
data: { cronCount: this.cronRegistry.size }
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
throw new Error(`Unknown action: ${actionName}`);
|
|
1360
|
+
}
|
|
1361
|
+
async statRoot(_ctx) {
|
|
1362
|
+
return { data: {
|
|
1363
|
+
id: "/",
|
|
1364
|
+
path: "/",
|
|
1365
|
+
meta: {
|
|
1366
|
+
type: "directory",
|
|
1367
|
+
childrenCount: 1,
|
|
1368
|
+
scriptCount: await this.getScriptCount()
|
|
1369
|
+
}
|
|
1370
|
+
} };
|
|
1371
|
+
}
|
|
1372
|
+
async statScriptsDir(_ctx) {
|
|
1373
|
+
return { data: {
|
|
1374
|
+
id: "/scripts",
|
|
1375
|
+
path: "/scripts",
|
|
1376
|
+
meta: {
|
|
1377
|
+
type: "directory",
|
|
1378
|
+
childrenCount: await this.getScriptCount()
|
|
1379
|
+
}
|
|
1380
|
+
} };
|
|
1381
|
+
}
|
|
1382
|
+
async statScript(ctx) {
|
|
1383
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1384
|
+
const script = await this.getScript(name);
|
|
1385
|
+
if (!script) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1386
|
+
return { data: {
|
|
1387
|
+
id: joinURL("/scripts", `${name}.ash`),
|
|
1388
|
+
path: joinURL("/scripts", `${name}.ash`),
|
|
1389
|
+
createdAt: script.createdAt,
|
|
1390
|
+
updatedAt: script.updatedAt,
|
|
1391
|
+
meta: {
|
|
1392
|
+
type: "file",
|
|
1393
|
+
kind: "ash:script",
|
|
1394
|
+
scriptName: name,
|
|
1395
|
+
size: new TextEncoder().encode(script.source).length
|
|
1396
|
+
}
|
|
1397
|
+
} };
|
|
1398
|
+
}
|
|
1399
|
+
async explainRoot(_ctx) {
|
|
1400
|
+
return {
|
|
1401
|
+
format: "markdown",
|
|
1402
|
+
content: await readDoc("agent.md")
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
async explainScript(ctx) {
|
|
1406
|
+
const name = ctx.params.scriptName.replace(/\.ash$/, "");
|
|
1407
|
+
const script = await this.getScript(name);
|
|
1408
|
+
if (!script) throw new AFSNotFoundError(ctx.path, `Script not found: ${name}`);
|
|
1409
|
+
const result = compileSource(script.source);
|
|
1410
|
+
const lines = [];
|
|
1411
|
+
lines.push(`# Script: ${name}.ash`);
|
|
1412
|
+
lines.push("");
|
|
1413
|
+
lines.push("## Source");
|
|
1414
|
+
lines.push("");
|
|
1415
|
+
lines.push("```ash");
|
|
1416
|
+
lines.push(script.source);
|
|
1417
|
+
lines.push("```");
|
|
1418
|
+
lines.push("");
|
|
1419
|
+
lines.push("## Analysis");
|
|
1420
|
+
lines.push("");
|
|
1421
|
+
lines.push(`- **Jobs**: ${result.program?.jobs.length ?? 0}`);
|
|
1422
|
+
lines.push(`- **Diagnostics**: ${result.diagnostics.length}`);
|
|
1423
|
+
lines.push(`- **Size**: ${new TextEncoder().encode(script.source).length} bytes`);
|
|
1424
|
+
lines.push("");
|
|
1425
|
+
return {
|
|
1426
|
+
format: "markdown",
|
|
1427
|
+
content: lines.join("\n")
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
async searchScripts(_ctx, query) {
|
|
1431
|
+
const results = [];
|
|
1432
|
+
const lowerQuery = query.toLowerCase();
|
|
1433
|
+
for (const name of await this.getAllScriptNames()) {
|
|
1434
|
+
const script = await this.getScript(name);
|
|
1435
|
+
if (!script) continue;
|
|
1436
|
+
if (name.toLowerCase().includes(lowerQuery) || script.source.toLowerCase().includes(lowerQuery)) results.push(this.buildScriptEntry(name, script));
|
|
1437
|
+
}
|
|
1438
|
+
return { data: results };
|
|
1439
|
+
}
|
|
1440
|
+
buildScriptEntry(name, script) {
|
|
1441
|
+
const inputSchema = this.buildScriptExecInputSchema(script.source);
|
|
1442
|
+
return {
|
|
1443
|
+
id: joinURL("/scripts", `${name}.ash`),
|
|
1444
|
+
path: joinURL("/scripts", `${name}.ash`),
|
|
1445
|
+
content: script.source,
|
|
1446
|
+
createdAt: script.createdAt,
|
|
1447
|
+
updatedAt: script.updatedAt,
|
|
1448
|
+
meta: {
|
|
1449
|
+
type: "file",
|
|
1450
|
+
kind: "ash:script",
|
|
1451
|
+
kinds: [
|
|
1452
|
+
"ash:script",
|
|
1453
|
+
"afs:executable",
|
|
1454
|
+
"afs:node"
|
|
1455
|
+
],
|
|
1456
|
+
scriptName: name,
|
|
1457
|
+
size: new TextEncoder().encode(script.source).length
|
|
1458
|
+
},
|
|
1459
|
+
actions: [{
|
|
1460
|
+
name: "exec",
|
|
1461
|
+
description: `Execute script ${name}`,
|
|
1462
|
+
inputSchema
|
|
1463
|
+
}]
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
buildScriptExecInputSchema(source) {
|
|
1467
|
+
const properties = { caps: {
|
|
1468
|
+
type: "array",
|
|
1469
|
+
items: { type: "string" },
|
|
1470
|
+
description: "Capability paths. Required for write-back to AFS (default: no write-back without caps)"
|
|
1471
|
+
} };
|
|
1472
|
+
const result = compileSource(source);
|
|
1473
|
+
if (result.program) for (const [key, value] of result.program.params) properties[key] = {
|
|
1474
|
+
type: typeof value === "number" ? "number" : "string",
|
|
1475
|
+
description: `Override ASH param '${key}' at execution time`
|
|
1476
|
+
};
|
|
1477
|
+
return {
|
|
1478
|
+
type: "object",
|
|
1479
|
+
properties
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Create an async-capable world that bridges ASH to AFS.
|
|
1484
|
+
* Since ASH's WorldInterface is synchronous but AFS is async,
|
|
1485
|
+
* we collect operations and replay them, or use a pre-loaded data approach.
|
|
1486
|
+
*/
|
|
1487
|
+
async createAsyncWorld(afsOverride) {
|
|
1488
|
+
const dataStore = { ...await this.loadPersistedData() };
|
|
1489
|
+
const written = {};
|
|
1490
|
+
const published = {};
|
|
1491
|
+
const writeCounts = {};
|
|
1492
|
+
const afsRoot = afsOverride ?? this.afsRoot;
|
|
1493
|
+
const selfPrefix = `/${this.name}/`;
|
|
1494
|
+
const allowRootActions = this.allowRootActions;
|
|
1495
|
+
const pendingPersist = [];
|
|
1496
|
+
const persistData = (path, data) => {
|
|
1497
|
+
pendingPersist.push(this.persistData(path, data).catch(() => {}));
|
|
1498
|
+
};
|
|
1499
|
+
return {
|
|
1500
|
+
world: {
|
|
1501
|
+
read(path) {
|
|
1502
|
+
return dataStore[normalizeAbsoluteAfsPath(path)] ?? [];
|
|
1503
|
+
},
|
|
1504
|
+
write(path, data) {
|
|
1505
|
+
const normalizedPath = normalizeAbsoluteAfsPath(path);
|
|
1506
|
+
if (normalizedPath.startsWith(selfPrefix) || normalizedPath.startsWith("/scripts") || normalizedPath.startsWith("/.actions") || normalizedPath.startsWith("/.meta")) throw new Error(`Write blocked: '${path}' is a protected ASH provider path`);
|
|
1507
|
+
writeCounts[normalizedPath] = (writeCounts[normalizedPath] ?? 0) + 1;
|
|
1508
|
+
if (written[normalizedPath]) written[normalizedPath].push(...data);
|
|
1509
|
+
else written[normalizedPath] = [...data];
|
|
1510
|
+
dataStore[normalizedPath] = written[normalizedPath];
|
|
1511
|
+
persistData(normalizedPath, data);
|
|
1512
|
+
},
|
|
1513
|
+
publish(topic, data) {
|
|
1514
|
+
published[topic] = data;
|
|
1515
|
+
},
|
|
1516
|
+
async exec(path, input, params) {
|
|
1517
|
+
const normalizedPath = normalizeAbsoluteAfsPath(path);
|
|
1518
|
+
const rootActionMatch = /^\/\.actions\/([^/]+)$/.exec(normalizedPath) ?? (/* @__PURE__ */ new RegExp(`^${selfPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.actions\\/([^/]+)$`)).exec(normalizedPath);
|
|
1519
|
+
if (!(rootActionMatch !== null && allowRootActions.has(rootActionMatch[1])) && (normalizedPath.startsWith(selfPrefix) || normalizedPath.startsWith("/.actions") || normalizedPath.startsWith("/scripts"))) throw new Error(`Exec blocked: self-referencing call to '${path}' would cause recursion`);
|
|
1520
|
+
if (afsRoot?.exec) {
|
|
1521
|
+
const baseParams = params ?? {};
|
|
1522
|
+
const records = input.filter((r) => typeof r === "object" && r !== null);
|
|
1523
|
+
const mergeContext = (inputArgs, outputData) => {
|
|
1524
|
+
const items = Array.isArray(outputData) ? outputData : [outputData];
|
|
1525
|
+
const { _scope_afs: _, ...context } = inputArgs;
|
|
1526
|
+
return items.map((d) => typeof d === "object" && d !== null ? {
|
|
1527
|
+
...context,
|
|
1528
|
+
...d
|
|
1529
|
+
} : d);
|
|
1530
|
+
};
|
|
1531
|
+
const execAndCheck = async (mergedArgs) => {
|
|
1532
|
+
const result = await afsRoot.exec(normalizedPath, mergedArgs, {});
|
|
1533
|
+
if (!result.success) {
|
|
1534
|
+
const errMsg = typeof result.error === "object" && result.error !== null ? result.error.message ?? JSON.stringify(result.error) : typeof result.data?.error === "string" ? result.data.error : `Action failed: ${normalizedPath}`;
|
|
1535
|
+
throw new Error(errMsg);
|
|
1536
|
+
}
|
|
1537
|
+
return mergeContext(mergedArgs, result.data);
|
|
1538
|
+
};
|
|
1539
|
+
if (records.length <= 1) {
|
|
1540
|
+
const mergedArgs = {
|
|
1541
|
+
...records[0] ?? {},
|
|
1542
|
+
...baseParams
|
|
1543
|
+
};
|
|
1544
|
+
if (afsOverride) mergedArgs._scope_afs = afsOverride;
|
|
1545
|
+
return execAndCheck(mergedArgs);
|
|
1546
|
+
}
|
|
1547
|
+
const results = [];
|
|
1548
|
+
for (const record of records) {
|
|
1549
|
+
const mergedArgs = {
|
|
1550
|
+
...record,
|
|
1551
|
+
...baseParams
|
|
1552
|
+
};
|
|
1553
|
+
if (afsOverride) mergedArgs._scope_afs = afsOverride;
|
|
1554
|
+
results.push(...await execAndCheck(mergedArgs));
|
|
1555
|
+
}
|
|
1556
|
+
return results;
|
|
1557
|
+
}
|
|
1558
|
+
return input;
|
|
1559
|
+
},
|
|
1560
|
+
async input(prompt) {
|
|
1561
|
+
if (afsRoot?.exec) try {
|
|
1562
|
+
const result = await afsRoot.exec("/ui/.actions/prompt", {
|
|
1563
|
+
message: prompt,
|
|
1564
|
+
type: "text"
|
|
1565
|
+
}, {});
|
|
1566
|
+
return String(result.data?.response ?? "");
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
if (err instanceof AFSNotFoundError) return "";
|
|
1569
|
+
throw err;
|
|
1570
|
+
}
|
|
1571
|
+
return "";
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
written,
|
|
1575
|
+
published,
|
|
1576
|
+
writeCounts,
|
|
1577
|
+
preload(path, data) {
|
|
1578
|
+
dataStore[path] = data;
|
|
1579
|
+
},
|
|
1580
|
+
async flushPersist() {
|
|
1581
|
+
await Promise.all(pendingPersist);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Execute a single named job from an ASH source.
|
|
1587
|
+
* Used by ProgramManager to run trigger jobs with event data.
|
|
1588
|
+
*/
|
|
1589
|
+
async runJob(source, jobName, args, options) {
|
|
1590
|
+
const result = compileSource(this.applyParamOverridesToSource(source, args));
|
|
1591
|
+
if (!result.program) return {
|
|
1592
|
+
success: false,
|
|
1593
|
+
data: {
|
|
1594
|
+
error: `Compilation failed: ${result.diagnostics.map((d) => d.message).join("; ") || "unknown error"}`,
|
|
1595
|
+
diagnostics: result.diagnostics
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
const job = result.program.units.find((u) => u.kind === "job" && u.name === jobName);
|
|
1599
|
+
if (!job || job.kind !== "job") return {
|
|
1600
|
+
success: false,
|
|
1601
|
+
data: { error: `Job "${jobName}" not found in source` }
|
|
1602
|
+
};
|
|
1603
|
+
const afsOverride = options?.afsOverride;
|
|
1604
|
+
const { world, written } = await this.createAsyncWorld(afsOverride);
|
|
1605
|
+
const effectiveAFS = afsOverride ?? this.afsRoot;
|
|
1606
|
+
const bridgedWorld = effectiveAFS ? {
|
|
1607
|
+
read: (path, query) => readAfsRecords(effectiveAFS, world, path, query),
|
|
1608
|
+
write: (path, data) => {
|
|
1609
|
+
world.write(path, data);
|
|
1610
|
+
},
|
|
1611
|
+
publish: (topic, data) => {
|
|
1612
|
+
world.publish(topic, data);
|
|
1613
|
+
},
|
|
1614
|
+
exec: world.exec
|
|
1615
|
+
} : world;
|
|
1616
|
+
let outputResult;
|
|
1617
|
+
const ctx = {
|
|
1618
|
+
world: bridgedWorld,
|
|
1619
|
+
caps: new Set(["*"]),
|
|
1620
|
+
logger: { log() {} },
|
|
1621
|
+
output: { output(event$1) {
|
|
1622
|
+
outputResult = event$1.content;
|
|
1623
|
+
} }
|
|
1624
|
+
};
|
|
1625
|
+
const event = args.event;
|
|
1626
|
+
const initialStream = event ? [{ ...event }] : [];
|
|
1627
|
+
const jobResult = await job.execute(ctx, initialStream);
|
|
1628
|
+
const afs = effectiveAFS;
|
|
1629
|
+
if (afs?.write) for (const [path, data] of Object.entries(written)) if (path.endsWith(".log")) {
|
|
1630
|
+
const content = `${data.map((item) => JSON.stringify(item)).join("\n")}\n`;
|
|
1631
|
+
await afs.write(path, { content }, { mode: "append" });
|
|
1632
|
+
} else await afs.write(path, { content: JSON.stringify(data) });
|
|
1633
|
+
if (jobResult.status !== "ok") return {
|
|
1634
|
+
success: false,
|
|
1635
|
+
data: {
|
|
1636
|
+
error: `Job "${jobName}" failed`,
|
|
1637
|
+
status: jobResult.status,
|
|
1638
|
+
errors: jobResult.errors
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
return {
|
|
1642
|
+
success: true,
|
|
1643
|
+
data: {
|
|
1644
|
+
job: jobName,
|
|
1645
|
+
recordCount: jobResult.recordCount,
|
|
1646
|
+
...outputResult !== void 0 ? { result: outputResult } : {}
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
async runAsh(source, args, options) {
|
|
1651
|
+
const result = compileSource(this.applyParamOverridesToSource(source, args));
|
|
1652
|
+
if (result.diagnostics.filter((d) => d.severity !== "warning").length > 0 || !result.program) {
|
|
1653
|
+
const sanitizedDiags = result.diagnostics.map((d) => ({
|
|
1654
|
+
...d,
|
|
1655
|
+
message: sanitizeErrorPath(d.message)
|
|
1656
|
+
}));
|
|
1657
|
+
return {
|
|
1658
|
+
success: false,
|
|
1659
|
+
data: {
|
|
1660
|
+
error: `Compilation failed: ${sanitizedDiags.filter((d) => d.severity !== "warning").map((d) => d.message).join("; ") || "unknown error"}`,
|
|
1661
|
+
diagnostics: sanitizedDiags,
|
|
1662
|
+
sourceHash: result.sourceHash
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
const expectedHash = args.expectedHash;
|
|
1667
|
+
if (typeof expectedHash === "string" && expectedHash !== result.sourceHash) return {
|
|
1668
|
+
success: false,
|
|
1669
|
+
data: {
|
|
1670
|
+
error: "Source hash mismatch — code was modified between validate and run (TOCTOU)",
|
|
1671
|
+
expectedHash,
|
|
1672
|
+
actualHash: result.sourceHash
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
const capsArg = args.caps;
|
|
1676
|
+
const caps = Array.isArray(capsArg) && capsArg.every((c) => typeof c === "string") ? new Set(capsArg) : new Set(["*"]);
|
|
1677
|
+
const afsOverride = options?.afsOverride;
|
|
1678
|
+
const { world, written, published, writeCounts, flushPersist } = await this.createAsyncWorld(afsOverride);
|
|
1679
|
+
const effectiveAFS = afsOverride ?? this.afsRoot;
|
|
1680
|
+
const bridgedWorld = effectiveAFS ? {
|
|
1681
|
+
read: (path, query) => readAfsRecords(effectiveAFS, world, path, query),
|
|
1682
|
+
write: (path, data) => {
|
|
1683
|
+
world.write(path, data);
|
|
1684
|
+
},
|
|
1685
|
+
publish: (topic, data) => {
|
|
1686
|
+
world.publish(topic, data);
|
|
1687
|
+
},
|
|
1688
|
+
exec: world.exec
|
|
1689
|
+
} : world;
|
|
1690
|
+
const logs = [];
|
|
1691
|
+
const logger = { log(stage, action, detail) {
|
|
1692
|
+
logs.push({
|
|
1693
|
+
stage,
|
|
1694
|
+
action,
|
|
1695
|
+
detail
|
|
1696
|
+
});
|
|
1697
|
+
} };
|
|
1698
|
+
const outputs = [];
|
|
1699
|
+
const ctx = {
|
|
1700
|
+
world: bridgedWorld,
|
|
1701
|
+
caps,
|
|
1702
|
+
logger,
|
|
1703
|
+
output: { output(event) {
|
|
1704
|
+
outputs.push(event.content);
|
|
1705
|
+
} }
|
|
1706
|
+
};
|
|
1707
|
+
const jobResults = [];
|
|
1708
|
+
for (const unit of result.program.units) if (unit.kind === "job") {
|
|
1709
|
+
if (result.program.routeTargets?.has(unit.name)) continue;
|
|
1710
|
+
if (unit.trigger) continue;
|
|
1711
|
+
jobResults.push(await unit.execute(ctx));
|
|
1712
|
+
} else await unit.execute(ctx);
|
|
1713
|
+
const sanitizedJobs = jobResults.map(({ stages, ...rest }) => rest);
|
|
1714
|
+
const allOk = jobResults.every((r) => r.status === "ok");
|
|
1715
|
+
const allErrors = jobResults.flatMap((r) => r.errors).map(sanitizeErrorPath);
|
|
1716
|
+
const afs = afsOverride ?? this.afsRoot;
|
|
1717
|
+
const skipPrefix = options?.skipWritePrefix;
|
|
1718
|
+
if (afs?.write && options?.allowWriteBack !== false) {
|
|
1719
|
+
for (const [path, data] of Object.entries(written)) {
|
|
1720
|
+
if (skipPrefix && path.startsWith(skipPrefix)) continue;
|
|
1721
|
+
try {
|
|
1722
|
+
if (path.endsWith(".log")) {
|
|
1723
|
+
const content = `${data.map((item) => JSON.stringify(item)).join("\n")}\n`;
|
|
1724
|
+
await afs.write(path, { content }, { mode: "append" });
|
|
1725
|
+
} else await afs.write(path, { content: JSON.stringify(data) });
|
|
1726
|
+
} catch {
|
|
1727
|
+
allErrors.push("Failed to write to AFS");
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
for (const [topic, data] of Object.entries(published)) try {
|
|
1731
|
+
await afs.write(topic, { content: JSON.stringify(data) });
|
|
1732
|
+
} catch {
|
|
1733
|
+
allErrors.push("Failed to publish to AFS");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const overwrittenPaths = Object.entries(writeCounts).filter(([, count]) => count > 1).map(([path, count]) => sanitizeErrorPath(`Path '${path}' was written ${count} times — only last write preserved`));
|
|
1737
|
+
if (overwrittenPaths.length > 0) allErrors.push(...overwrittenPaths);
|
|
1738
|
+
await flushPersist();
|
|
1739
|
+
return {
|
|
1740
|
+
success: allOk && allErrors.length === 0 && overwrittenPaths.length === 0,
|
|
1741
|
+
data: {
|
|
1742
|
+
jobs: sanitizedJobs,
|
|
1743
|
+
written: Object.keys(written),
|
|
1744
|
+
published: Object.keys(published),
|
|
1745
|
+
outputs,
|
|
1746
|
+
errors: allErrors.length > 0 ? allErrors : void 0,
|
|
1747
|
+
warnings: result.diagnostics.filter((d) => d.severity === "warning").map((d) => ({
|
|
1748
|
+
...d,
|
|
1749
|
+
message: sanitizeErrorPath(d.message)
|
|
1750
|
+
})),
|
|
1751
|
+
sourceHash: result.sourceHash,
|
|
1752
|
+
...options?.returnWrittenData ? { writtenData: written } : {}
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
applyParamOverridesToSource(source, args) {
|
|
1757
|
+
const lines = source.split("\n");
|
|
1758
|
+
let changed = false;
|
|
1759
|
+
const replaced = lines.map((line) => {
|
|
1760
|
+
const match = /^(\s*param\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*)(.+)$/.exec(line);
|
|
1761
|
+
if (!match) return line;
|
|
1762
|
+
const [, prefix, name] = match;
|
|
1763
|
+
if (!name) return line;
|
|
1764
|
+
const value = args[name];
|
|
1765
|
+
if (typeof value !== "string" && typeof value !== "number") return line;
|
|
1766
|
+
changed = true;
|
|
1767
|
+
return `${prefix}${typeof value === "string" ? JSON.stringify(value) : String(value)}`;
|
|
1768
|
+
});
|
|
1769
|
+
return changed ? replaced.join("\n") : source;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
__decorate([List("/")], AFSAsh.prototype, "listRoot", null);
|
|
1773
|
+
__decorate([List("/scripts")], AFSAsh.prototype, "listScripts", null);
|
|
1774
|
+
__decorate([List("/scripts/:scriptName")], AFSAsh.prototype, "listScript", null);
|
|
1775
|
+
__decorate([Actions("/")], AFSAsh.prototype, "listRootActions", null);
|
|
1776
|
+
__decorate([Actions("/scripts/:scriptName")], AFSAsh.prototype, "listScriptActions", null);
|
|
1777
|
+
__decorate([Read("/")], AFSAsh.prototype, "readRoot", null);
|
|
1778
|
+
__decorate([Read("/scripts")], AFSAsh.prototype, "readScriptsDir", null);
|
|
1779
|
+
__decorate([Read("/scripts/:scriptName")], AFSAsh.prototype, "readScript", null);
|
|
1780
|
+
__decorate([List("/cookbook")], AFSAsh.prototype, "listCookbook", null);
|
|
1781
|
+
__decorate([Read("/cookbook")], AFSAsh.prototype, "readCookbookDir", null);
|
|
1782
|
+
__decorate([List("/cookbook/:file")], AFSAsh.prototype, "listCookbookFile", null);
|
|
1783
|
+
__decorate([Read("/cookbook/:file")], AFSAsh.prototype, "readCookbook", null);
|
|
1784
|
+
__decorate([Read("/.meta/agent.md")], AFSAsh.prototype, "readAgentMd", null);
|
|
1785
|
+
__decorate([Read("/.meta/.ash-reference")], AFSAsh.prototype, "readAshReference", null);
|
|
1786
|
+
__decorate([Read("/.meta/.capabilities")], AFSAsh.prototype, "readCapabilities", null);
|
|
1787
|
+
__decorate([Meta("/")], AFSAsh.prototype, "readRootMeta", null);
|
|
1788
|
+
__decorate([Meta("/cookbook")], AFSAsh.prototype, "readCookbookMeta", null);
|
|
1789
|
+
__decorate([Meta("/cookbook/:file")], AFSAsh.prototype, "readCookbookFileMeta", null);
|
|
1790
|
+
__decorate([Meta("/scripts")], AFSAsh.prototype, "readScriptsMeta", null);
|
|
1791
|
+
__decorate([Meta("/scripts/:scriptName")], AFSAsh.prototype, "readScriptMeta", null);
|
|
1792
|
+
__decorate([Write("/scripts/:scriptName")], AFSAsh.prototype, "writeScript", null);
|
|
1793
|
+
__decorate([Delete("/scripts/:scriptName")], AFSAsh.prototype, "deleteScript", null);
|
|
1794
|
+
__decorate([Delete("/:path*")], AFSAsh.prototype, "deleteCatchAll", null);
|
|
1795
|
+
__decorate([Actions.Exec("/scripts/:scriptName", "exec", void 0, { effect: "write" })], AFSAsh.prototype, "execScript", null);
|
|
1796
|
+
__decorate([Exec("/scripts/:scriptName")], AFSAsh.prototype, "execScriptDirect", null);
|
|
1797
|
+
__decorate([Actions.Exec("/", void 0, void 0, { effect: "write" })], AFSAsh.prototype, "execRootAction", null);
|
|
1798
|
+
__decorate([Stat("/")], AFSAsh.prototype, "statRoot", null);
|
|
1799
|
+
__decorate([Stat("/scripts")], AFSAsh.prototype, "statScriptsDir", null);
|
|
1800
|
+
__decorate([Stat("/scripts/:scriptName")], AFSAsh.prototype, "statScript", null);
|
|
1801
|
+
__decorate([Explain("/")], AFSAsh.prototype, "explainRoot", null);
|
|
1802
|
+
__decorate([Explain("/scripts/:scriptName")], AFSAsh.prototype, "explainScript", null);
|
|
1803
|
+
__decorate([Search("/:path*")], AFSAsh.prototype, "searchScripts", null);
|
|
1804
|
+
|
|
1805
|
+
//#endregion
|
|
1806
|
+
export { intersectTools, validateActionPath, validateToolCall, withQueryPairing };
|
|
1807
|
+
//# sourceMappingURL=index.mjs.map
|