ai-runtime-engine 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/LICENSE +15 -0
- package/README.md +489 -0
- package/dist/artifacts/artifacts.d.ts +28 -0
- package/dist/artifacts/artifacts.js +46 -0
- package/dist/benchmark/benchmark.d.ts +23 -0
- package/dist/benchmark/benchmark.js +40 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.js +161 -0
- package/dist/cli/commands/cleanup.d.ts +47 -0
- package/dist/cli/commands/cleanup.js +96 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/doctor.d.ts +57 -0
- package/dist/cli/commands/doctor.js +86 -0
- package/dist/cli/commands/executions.d.ts +9 -0
- package/dist/cli/commands/executions.js +25 -0
- package/dist/cli/commands/info.d.ts +43 -0
- package/dist/cli/commands/info.js +53 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +75 -0
- package/dist/cli/commands/inspect.d.ts +16 -0
- package/dist/cli/commands/inspect.js +60 -0
- package/dist/cli/commands/phase2.d.ts +22 -0
- package/dist/cli/commands/phase2.js +83 -0
- package/dist/cli/commands/route.d.ts +14 -0
- package/dist/cli/commands/route.js +49 -0
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +37 -0
- package/dist/cli/commands/setup.d.ts +34 -0
- package/dist/cli/commands/setup.js +104 -0
- package/dist/cli/commands/skills.d.ts +28 -0
- package/dist/cli/commands/skills.js +48 -0
- package/dist/cli/commands/test.d.ts +7 -0
- package/dist/cli/commands/test.js +29 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +16 -0
- package/dist/cli/interactive/repl.d.ts +6 -0
- package/dist/cli/interactive/repl.js +45 -0
- package/dist/cli/interactive/session.d.ts +36 -0
- package/dist/cli/interactive/session.js +356 -0
- package/dist/cli/prompt.d.ts +6 -0
- package/dist/cli/prompt.js +18 -0
- package/dist/cli/render.d.ts +7 -0
- package/dist/cli/render.js +14 -0
- package/dist/comparison/analysis.d.ts +46 -0
- package/dist/comparison/analysis.js +177 -0
- package/dist/comparison/comparator.d.ts +46 -0
- package/dist/comparison/comparator.js +270 -0
- package/dist/comparison/comparison.d.ts +140 -0
- package/dist/comparison/comparison.js +9 -0
- package/dist/comparison/render.d.ts +7 -0
- package/dist/comparison/render.js +66 -0
- package/dist/config/defaults.d.ts +52 -0
- package/dist/config/defaults.js +56 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +50 -0
- package/dist/config/providerDefaults.d.ts +17 -0
- package/dist/config/providerDefaults.js +61 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/schema.js +78 -0
- package/dist/context/budget.d.ts +13 -0
- package/dist/context/budget.js +17 -0
- package/dist/context/compiler.d.ts +61 -0
- package/dist/context/compiler.js +125 -0
- package/dist/context/tokens.d.ts +19 -0
- package/dist/context/tokens.js +38 -0
- package/dist/conversations/conversations.d.ts +38 -0
- package/dist/conversations/conversations.js +64 -0
- package/dist/core/capabilities/evidence.d.ts +40 -0
- package/dist/core/capabilities/evidence.js +102 -0
- package/dist/core/capabilities/overlay.d.ts +15 -0
- package/dist/core/capabilities/overlay.js +0 -0
- package/dist/core/capabilities/taxonomy.d.ts +19 -0
- package/dist/core/capabilities/taxonomy.js +25 -0
- package/dist/core/fallback/errors.d.ts +30 -0
- package/dist/core/fallback/errors.js +80 -0
- package/dist/core/fallback/fallback.d.ts +40 -0
- package/dist/core/fallback/fallback.js +82 -0
- package/dist/core/fallback/retryPolicy.d.ts +11 -0
- package/dist/core/fallback/retryPolicy.js +14 -0
- package/dist/core/health/health.d.ts +3 -0
- package/dist/core/health/health.js +5 -0
- package/dist/core/health/monitor.d.ts +23 -0
- package/dist/core/health/monitor.js +82 -0
- package/dist/core/policies/budget.d.ts +19 -0
- package/dist/core/policies/budget.js +37 -0
- package/dist/core/registry/builtinTasks.d.ts +8 -0
- package/dist/core/registry/builtinTasks.js +54 -0
- package/dist/core/registry/registry.d.ts +18 -0
- package/dist/core/registry/registry.js +33 -0
- package/dist/core/registry/taskRegistry.d.ts +15 -0
- package/dist/core/registry/taskRegistry.js +30 -0
- package/dist/core/router/confidence.d.ts +7 -0
- package/dist/core/router/confidence.js +20 -0
- package/dist/core/router/dimensions.d.ts +16 -0
- package/dist/core/router/dimensions.js +60 -0
- package/dist/core/router/executor.d.ts +16 -0
- package/dist/core/router/executor.js +25 -0
- package/dist/core/router/filter.d.ts +34 -0
- package/dist/core/router/filter.js +113 -0
- package/dist/core/router/normalize.d.ts +30 -0
- package/dist/core/router/normalize.js +119 -0
- package/dist/core/router/request.d.ts +4 -0
- package/dist/core/router/request.js +21 -0
- package/dist/core/router/router.d.ts +32 -0
- package/dist/core/router/router.js +195 -0
- package/dist/core/router/routingPrefs.d.ts +11 -0
- package/dist/core/router/routingPrefs.js +30 -0
- package/dist/core/router/scorer.d.ts +19 -0
- package/dist/core/router/scorer.js +50 -0
- package/dist/core/router/weights.d.ts +9 -0
- package/dist/core/router/weights.js +31 -0
- package/dist/core/validation/validator.d.ts +16 -0
- package/dist/core/validation/validator.js +33 -0
- package/dist/discovery/modelCatalog.d.ts +28 -0
- package/dist/discovery/modelCatalog.js +105 -0
- package/dist/discovery/openapi.d.ts +25 -0
- package/dist/discovery/openapi.js +76 -0
- package/dist/executions/checkpoint.d.ts +26 -0
- package/dist/executions/checkpoint.js +114 -0
- package/dist/executions/execution.d.ts +51 -0
- package/dist/executions/execution.js +8 -0
- package/dist/executions/store.d.ts +52 -0
- package/dist/executions/store.js +124 -0
- package/dist/generation/generateAdapter.d.ts +17 -0
- package/dist/generation/generateAdapter.js +30 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +107 -0
- package/dist/learning/feedback.d.ts +9 -0
- package/dist/learning/feedback.js +18 -0
- package/dist/learning/learningStore.d.ts +68 -0
- package/dist/learning/learningStore.js +138 -0
- package/dist/learning/performanceStore.d.ts +27 -0
- package/dist/learning/performanceStore.js +0 -0
- package/dist/marketplace/presets.d.ts +24 -0
- package/dist/marketplace/presets.js +52 -0
- package/dist/mcp/mcp.d.ts +31 -0
- package/dist/mcp/mcp.js +54 -0
- package/dist/memory/bm25.d.ts +16 -0
- package/dist/memory/bm25.js +56 -0
- package/dist/memory/classifier.d.ts +14 -0
- package/dist/memory/classifier.js +17 -0
- package/dist/memory/memory.d.ts +80 -0
- package/dist/memory/memory.js +191 -0
- package/dist/orchestration/executor.d.ts +35 -0
- package/dist/orchestration/executor.js +65 -0
- package/dist/orchestration/orchestrator.d.ts +42 -0
- package/dist/orchestration/orchestrator.js +63 -0
- package/dist/orchestration/plan.d.ts +37 -0
- package/dist/orchestration/plan.js +70 -0
- package/dist/orchestration/planner.d.ts +29 -0
- package/dist/orchestration/planner.js +69 -0
- package/dist/plugin/ai.d.ts +82 -0
- package/dist/plugin/ai.js +167 -0
- package/dist/probing/probe.d.ts +25 -0
- package/dist/probing/probe.js +63 -0
- package/dist/providers/factory.d.ts +18 -0
- package/dist/providers/factory.js +54 -0
- package/dist/providers/httpClient.d.ts +34 -0
- package/dist/providers/httpClient.js +80 -0
- package/dist/providers/httpProvider.d.ts +49 -0
- package/dist/providers/httpProvider.js +135 -0
- package/dist/providers/mock/demo.d.ts +13 -0
- package/dist/providers/mock/demo.js +58 -0
- package/dist/providers/mock/mockProvider.d.ts +35 -0
- package/dist/providers/mock/mockProvider.js +121 -0
- package/dist/providers/mock/scenarios.d.ts +44 -0
- package/dist/providers/mock/scenarios.js +30 -0
- package/dist/providers/provider.d.ts +26 -0
- package/dist/providers/provider.js +11 -0
- package/dist/providers/wire/anthropicWire.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +83 -0
- package/dist/providers/wire/openaiWire.d.ts +7 -0
- package/dist/providers/wire/openaiWire.js +81 -0
- package/dist/providers/wire/registry.d.ts +8 -0
- package/dist/providers/wire/registry.js +20 -0
- package/dist/providers/wire/types.d.ts +39 -0
- package/dist/providers/wire/types.js +24 -0
- package/dist/runtime/config.d.ts +31 -0
- package/dist/runtime/config.js +121 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +11 -0
- package/dist/runtime/events.d.ts +99 -0
- package/dist/runtime/events.js +82 -0
- package/dist/runtime/host.d.ts +27 -0
- package/dist/runtime/host.js +7 -0
- package/dist/runtime/intent/classifier.d.ts +30 -0
- package/dist/runtime/intent/classifier.js +60 -0
- package/dist/runtime/intent/signals.d.ts +19 -0
- package/dist/runtime/intent/signals.js +46 -0
- package/dist/runtime/modes/availability.d.ts +11 -0
- package/dist/runtime/modes/availability.js +17 -0
- package/dist/runtime/modes/chat.d.ts +18 -0
- package/dist/runtime/modes/chat.js +67 -0
- package/dist/runtime/modes/modeResolver.d.ts +43 -0
- package/dist/runtime/modes/modeResolver.js +78 -0
- package/dist/runtime/policy.d.ts +72 -0
- package/dist/runtime/policy.js +59 -0
- package/dist/runtime/providerView.d.ts +62 -0
- package/dist/runtime/providerView.js +105 -0
- package/dist/runtime/routing.d.ts +26 -0
- package/dist/runtime/routing.js +65 -0
- package/dist/runtime/runtime.d.ts +191 -0
- package/dist/runtime/runtime.js +718 -0
- package/dist/runtime/types.d.ts +153 -0
- package/dist/runtime/types.js +9 -0
- package/dist/runtime/workspace/detectors.d.ts +15 -0
- package/dist/runtime/workspace/detectors.js +57 -0
- package/dist/runtime/workspace/workspace.d.ts +29 -0
- package/dist/runtime/workspace/workspace.js +116 -0
- package/dist/security/credentials.d.ts +26 -0
- package/dist/security/credentials.js +34 -0
- package/dist/security/redact.d.ts +16 -0
- package/dist/security/redact.js +57 -0
- package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
- package/dist/skills/builtins/fileAnalyzer.js +47 -0
- package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
- package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
- package/dist/skills/discovery.d.ts +61 -0
- package/dist/skills/discovery.js +211 -0
- package/dist/skills/manifest.d.ts +30 -0
- package/dist/skills/manifest.js +75 -0
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.js +22 -0
- package/dist/skills/skill.d.ts +64 -0
- package/dist/skills/skill.js +8 -0
- package/dist/store/area.d.ts +54 -0
- package/dist/store/area.js +164 -0
- package/dist/store/paths.d.ts +15 -0
- package/dist/store/paths.js +48 -0
- package/dist/store/store.d.ts +59 -0
- package/dist/store/store.js +140 -0
- package/dist/telemetry/sinks/file.d.ts +12 -0
- package/dist/telemetry/sinks/file.js +28 -0
- package/dist/telemetry/telemetry.d.ts +36 -0
- package/dist/telemetry/telemetry.js +63 -0
- package/dist/tools/builtins/filesystem.d.ts +7 -0
- package/dist/tools/builtins/filesystem.js +53 -0
- package/dist/tools/builtins/git.d.ts +10 -0
- package/dist/tools/builtins/git.js +66 -0
- package/dist/tools/builtins/shell.d.ts +17 -0
- package/dist/tools/builtins/shell.js +91 -0
- package/dist/tools/jail.d.ts +12 -0
- package/dist/tools/jail.js +98 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +24 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +20 -0
- package/dist/tools/runner.d.ts +23 -0
- package/dist/tools/runner.js +64 -0
- package/dist/tools/tool.d.ts +53 -0
- package/dist/tools/tool.js +24 -0
- package/dist/tools/untrusted.d.ts +13 -0
- package/dist/tools/untrusted.js +30 -0
- package/dist/types.d.ts +460 -0
- package/dist/types.js +12 -0
- package/dist/util/clock.d.ts +6 -0
- package/dist/util/clock.js +4 -0
- package/dist/util/extractJson.d.ts +8 -0
- package/dist/util/extractJson.js +54 -0
- package/dist/verification/verify.d.ts +26 -0
- package/dist/verification/verify.js +67 -0
- package/docs/GUIDE.md +358 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +78 -0
- package/docs/router.md +376 -0
- package/docs/security.md +55 -0
- package/package.json +67 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single managed directory in the runtime store. JSON records are written atomically (tmp + rename)
|
|
3
|
+
* with a sha256 integrity wrapper so corruption is detectable; append logs are JSONL. `NullArea` is the
|
|
4
|
+
* no-op used in stateless mode. Ids are validated to prevent path traversal.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const STORE_VERSION = 1;
|
|
10
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
11
|
+
/** Matches an orphaned atomic-write temp file: `<id>.json.tmp-<pid>-<n>` / `<id>.jsonl.tmp-…`. */
|
|
12
|
+
const TEMP_RE = /\.(?:json|jsonl)\.tmp-/;
|
|
13
|
+
function assertId(id) {
|
|
14
|
+
if (!ID_RE.test(id))
|
|
15
|
+
throw new Error(`invalid store id: ${JSON.stringify(id)}`);
|
|
16
|
+
}
|
|
17
|
+
function checksum(data) {
|
|
18
|
+
return createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
export class FileArea {
|
|
21
|
+
dir;
|
|
22
|
+
enabled = true;
|
|
23
|
+
ensured = false;
|
|
24
|
+
counter = 0;
|
|
25
|
+
constructor(dir) {
|
|
26
|
+
this.dir = dir;
|
|
27
|
+
}
|
|
28
|
+
ensure() {
|
|
29
|
+
if (!this.ensured) {
|
|
30
|
+
mkdirSync(this.dir, { recursive: true });
|
|
31
|
+
this.ensured = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
jsonPath(id) {
|
|
35
|
+
return join(this.dir, `${id}.json`);
|
|
36
|
+
}
|
|
37
|
+
logPath(id) {
|
|
38
|
+
return join(this.dir, `${id}.jsonl`);
|
|
39
|
+
}
|
|
40
|
+
atomicWrite(path, contents) {
|
|
41
|
+
this.ensure();
|
|
42
|
+
this.counter += 1;
|
|
43
|
+
const tmp = `${path}.tmp-${process.pid}-${this.counter}`;
|
|
44
|
+
writeFileSync(tmp, contents);
|
|
45
|
+
renameSync(tmp, path);
|
|
46
|
+
}
|
|
47
|
+
readJson(id) {
|
|
48
|
+
assertId(id);
|
|
49
|
+
const path = this.jsonPath(id);
|
|
50
|
+
if (!existsSync(path))
|
|
51
|
+
return undefined;
|
|
52
|
+
const wrapper = JSON.parse(readFileSync(path, 'utf8'));
|
|
53
|
+
if (wrapper.checksum !== checksum(wrapper.data))
|
|
54
|
+
throw new Error(`integrity check failed for ${path}`);
|
|
55
|
+
return wrapper.data;
|
|
56
|
+
}
|
|
57
|
+
tryReadJson(id) {
|
|
58
|
+
try {
|
|
59
|
+
return this.readJson(id);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined; // a corrupt record must not brick bulk listing/search
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
writeJson(id, data) {
|
|
66
|
+
assertId(id);
|
|
67
|
+
const wrapper = { v: STORE_VERSION, checksum: checksum(data), data };
|
|
68
|
+
this.atomicWrite(this.jsonPath(id), JSON.stringify(wrapper));
|
|
69
|
+
}
|
|
70
|
+
remove(id) {
|
|
71
|
+
assertId(id);
|
|
72
|
+
for (const p of [this.jsonPath(id), this.logPath(id)])
|
|
73
|
+
if (existsSync(p))
|
|
74
|
+
rmSync(p);
|
|
75
|
+
}
|
|
76
|
+
listIds() {
|
|
77
|
+
if (!existsSync(this.dir))
|
|
78
|
+
return [];
|
|
79
|
+
// `endsWith('.json')` already excludes temp files (they end `.json.tmp-<n>`), so a valid id that
|
|
80
|
+
// merely contains ".tmp-" is not wrongly hidden.
|
|
81
|
+
return readdirSync(this.dir)
|
|
82
|
+
.filter((f) => f.endsWith('.json'))
|
|
83
|
+
.map((f) => f.slice(0, -'.json'.length));
|
|
84
|
+
}
|
|
85
|
+
appendLine(id, obj) {
|
|
86
|
+
assertId(id);
|
|
87
|
+
this.ensure();
|
|
88
|
+
appendFileSync(this.logPath(id), JSON.stringify(obj) + '\n');
|
|
89
|
+
}
|
|
90
|
+
readLines(id) {
|
|
91
|
+
assertId(id);
|
|
92
|
+
const path = this.logPath(id);
|
|
93
|
+
if (!existsSync(path))
|
|
94
|
+
return [];
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
97
|
+
if (!line)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
out.push(JSON.parse(line));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* skip a corrupt line rather than failing the whole log (surfaced by check()) */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
check() {
|
|
109
|
+
if (!existsSync(this.dir))
|
|
110
|
+
return [];
|
|
111
|
+
const issues = [];
|
|
112
|
+
for (const f of readdirSync(this.dir)) {
|
|
113
|
+
const path = join(this.dir, f);
|
|
114
|
+
if (TEMP_RE.test(f)) {
|
|
115
|
+
issues.push({ file: path, problem: 'unreadable', detail: 'orphaned temp file' });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (f.endsWith('.json')) {
|
|
119
|
+
try {
|
|
120
|
+
const wrapper = JSON.parse(readFileSync(path, 'utf8'));
|
|
121
|
+
if (wrapper.checksum !== checksum(wrapper.data))
|
|
122
|
+
issues.push({ file: path, problem: 'checksum-mismatch' });
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
issues.push({ file: path, problem: 'invalid-json', detail: err instanceof Error ? err.message : String(err) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (f.endsWith('.jsonl')) {
|
|
129
|
+
const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
|
|
130
|
+
lines.forEach((line, i) => {
|
|
131
|
+
try {
|
|
132
|
+
JSON.parse(line);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
issues.push({ file: path, problem: 'invalid-json', detail: `line ${i + 1}` });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return issues;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** No-op area used in stateless mode. */
|
|
144
|
+
export class NullArea {
|
|
145
|
+
enabled = false;
|
|
146
|
+
readJson() {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
tryReadJson() {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
writeJson() { }
|
|
153
|
+
remove() { }
|
|
154
|
+
listIds() {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
appendLine() { }
|
|
158
|
+
readLines() {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
check() {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local runtime-store path + identity resolution. All persistent state lives under `~/.ai-runtime/`
|
|
3
|
+
* (override with AI_RUNTIME_HOME), NEVER inside the user's repository. Project identity is derived from
|
|
4
|
+
* the absolute workspace root, so sibling packages in a monorepo get distinct project stores; a shared
|
|
5
|
+
* `repository` identity (the git repo root) backs repository-scoped memory across those packages.
|
|
6
|
+
*/
|
|
7
|
+
/** Where a memory record physically lives. */
|
|
8
|
+
export type StoreScope = 'user' | 'project' | 'repository';
|
|
9
|
+
export declare function resolveHome(env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
/** Walk up from a directory to find the git repo root (dir containing `.git`). Undefined if none. */
|
|
11
|
+
export declare function findRepoRoot(start: string): string | undefined;
|
|
12
|
+
/** Stable, human-readable project id for the workspace root: `<slug>-<hash>`. */
|
|
13
|
+
export declare function projectId(root: string, name?: string): string;
|
|
14
|
+
/** Stable id for the enclosing git repository (falls back to the project id when not a repo). */
|
|
15
|
+
export declare function repositoryId(root: string, name?: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local runtime-store path + identity resolution. All persistent state lives under `~/.ai-runtime/`
|
|
3
|
+
* (override with AI_RUNTIME_HOME), NEVER inside the user's repository. Project identity is derived from
|
|
4
|
+
* the absolute workspace root, so sibling packages in a monorepo get distinct project stores; a shared
|
|
5
|
+
* `repository` identity (the git repo root) backs repository-scoped memory across those packages.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
11
|
+
export function resolveHome(env = process.env) {
|
|
12
|
+
const override = env.AI_RUNTIME_HOME;
|
|
13
|
+
return override && override.trim() ? resolve(override) : join(homedir(), '.ai-runtime');
|
|
14
|
+
}
|
|
15
|
+
function slug(s) {
|
|
16
|
+
return (s || 'project')
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '')
|
|
20
|
+
.slice(0, 32) || 'project';
|
|
21
|
+
}
|
|
22
|
+
function hash8(s) {
|
|
23
|
+
return createHash('sha256').update(s).digest('hex').slice(0, 12);
|
|
24
|
+
}
|
|
25
|
+
/** Walk up from a directory to find the git repo root (dir containing `.git`). Undefined if none. */
|
|
26
|
+
export function findRepoRoot(start) {
|
|
27
|
+
let dir = resolve(start);
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (existsSync(join(dir, '.git')))
|
|
30
|
+
return dir;
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir)
|
|
33
|
+
return undefined;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Stable, human-readable project id for the workspace root: `<slug>-<hash>`. */
|
|
38
|
+
export function projectId(root, name) {
|
|
39
|
+
const abs = resolve(root);
|
|
40
|
+
return `${slug(name ?? basename(abs))}-${hash8(abs)}`;
|
|
41
|
+
}
|
|
42
|
+
/** Stable id for the enclosing git repository (falls back to the project id when not a repo). */
|
|
43
|
+
export function repositoryId(root, name) {
|
|
44
|
+
const repo = findRepoRoot(root);
|
|
45
|
+
if (!repo)
|
|
46
|
+
return projectId(root, name);
|
|
47
|
+
return `${slug(basename(repo))}-${hash8(repo)}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeStore — the factory that maps the local home dir + project/repository identity to areas
|
|
3
|
+
* (memory per scope, conversations, indexes, cache, …). Stamps a store version, offers an advisory
|
|
4
|
+
* per-project lock, and an aggregate integrity check. In stateless mode every area is a NullArea.
|
|
5
|
+
*/
|
|
6
|
+
import type { Clock } from '../util/clock.js';
|
|
7
|
+
import type { Area, IntegrityIssue } from './area.js';
|
|
8
|
+
import type { StoreScope } from './paths.js';
|
|
9
|
+
export declare const STORE_VERSION = 1;
|
|
10
|
+
export interface RuntimeStoreOptions {
|
|
11
|
+
workspaceRoot: string;
|
|
12
|
+
workspaceName?: string;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
clock?: Clock;
|
|
15
|
+
/** When false, a stateless (in-memory / no-op) store is returned. */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface StorePaths {
|
|
19
|
+
home: string;
|
|
20
|
+
project: string;
|
|
21
|
+
repository: string;
|
|
22
|
+
user: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class RuntimeStore {
|
|
25
|
+
readonly enabled: boolean;
|
|
26
|
+
readonly projectId: string;
|
|
27
|
+
readonly repositoryId: string;
|
|
28
|
+
private readonly clock;
|
|
29
|
+
private readonly home;
|
|
30
|
+
private readonly projectDir;
|
|
31
|
+
private readonly repoDir;
|
|
32
|
+
private readonly userDir;
|
|
33
|
+
private readonly areas;
|
|
34
|
+
private metaStamped;
|
|
35
|
+
constructor(options: RuntimeStoreOptions);
|
|
36
|
+
paths(): StorePaths;
|
|
37
|
+
private area;
|
|
38
|
+
/** Memory area for a physical scope. */
|
|
39
|
+
memory(scope: StoreScope): Area;
|
|
40
|
+
conversations(): Area;
|
|
41
|
+
indexes(): Area;
|
|
42
|
+
cache(): Area;
|
|
43
|
+
executions(): Area;
|
|
44
|
+
artifacts(): Area;
|
|
45
|
+
/** Learning is store-wide (provider/skill/workflow/plan outcomes apply across projects). */
|
|
46
|
+
learning(): Area;
|
|
47
|
+
preferences(): Area;
|
|
48
|
+
/** Write the version stamp once (creates the project dir). No-op when stateless. */
|
|
49
|
+
stampMeta(): void;
|
|
50
|
+
/** The stored version, or undefined when absent — a migration hook seam for future versions. */
|
|
51
|
+
storedVersion(): number | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Advisory per-project lock. Best-effort: acquires an exclusive lock file, steals a stale one, and
|
|
54
|
+
* otherwise proceeds anyway (advisory). Hard mutual exclusion for executions is a Phase 7 lease.
|
|
55
|
+
*/
|
|
56
|
+
withLock<T>(fn: () => T, staleMs?: number): T;
|
|
57
|
+
/** Aggregate integrity check across the project areas. */
|
|
58
|
+
check(): IntegrityIssue[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeStore — the factory that maps the local home dir + project/repository identity to areas
|
|
3
|
+
* (memory per scope, conversations, indexes, cache, …). Stamps a store version, offers an advisory
|
|
4
|
+
* per-project lock, and an aggregate integrity check. In stateless mode every area is a NullArea.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { systemClock } from '../util/clock.js';
|
|
9
|
+
import { FileArea, NullArea } from './area.js';
|
|
10
|
+
import { projectId as computeProjectId, repositoryId as computeRepositoryId, resolveHome } from './paths.js';
|
|
11
|
+
export const STORE_VERSION = 1;
|
|
12
|
+
export class RuntimeStore {
|
|
13
|
+
enabled;
|
|
14
|
+
projectId;
|
|
15
|
+
repositoryId;
|
|
16
|
+
clock;
|
|
17
|
+
home;
|
|
18
|
+
projectDir;
|
|
19
|
+
repoDir;
|
|
20
|
+
userDir;
|
|
21
|
+
areas = new Map();
|
|
22
|
+
metaStamped = false;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
this.enabled = options.enabled !== false;
|
|
26
|
+
this.clock = options.clock ?? systemClock;
|
|
27
|
+
this.home = resolveHome(env);
|
|
28
|
+
this.projectId = computeProjectId(options.workspaceRoot, options.workspaceName);
|
|
29
|
+
this.repositoryId = computeRepositoryId(options.workspaceRoot, options.workspaceName);
|
|
30
|
+
this.projectDir = join(this.home, 'projects', this.projectId);
|
|
31
|
+
this.repoDir = join(this.home, 'repos', this.repositoryId);
|
|
32
|
+
this.userDir = join(this.home, 'user');
|
|
33
|
+
}
|
|
34
|
+
paths() {
|
|
35
|
+
return { home: this.home, project: this.projectDir, repository: this.repoDir, user: this.userDir };
|
|
36
|
+
}
|
|
37
|
+
area(dir) {
|
|
38
|
+
if (!this.enabled)
|
|
39
|
+
return new NullArea();
|
|
40
|
+
let a = this.areas.get(dir);
|
|
41
|
+
if (!a) {
|
|
42
|
+
a = new FileArea(dir);
|
|
43
|
+
this.areas.set(dir, a);
|
|
44
|
+
}
|
|
45
|
+
return a;
|
|
46
|
+
}
|
|
47
|
+
/** Memory area for a physical scope. */
|
|
48
|
+
memory(scope) {
|
|
49
|
+
if (scope === 'user')
|
|
50
|
+
return this.area(join(this.userDir, 'memory'));
|
|
51
|
+
if (scope === 'repository')
|
|
52
|
+
return this.area(join(this.repoDir, 'memory'));
|
|
53
|
+
return this.area(join(this.projectDir, 'memory'));
|
|
54
|
+
}
|
|
55
|
+
conversations() {
|
|
56
|
+
return this.area(join(this.projectDir, 'conversations'));
|
|
57
|
+
}
|
|
58
|
+
indexes() {
|
|
59
|
+
return this.area(join(this.projectDir, 'indexes'));
|
|
60
|
+
}
|
|
61
|
+
cache() {
|
|
62
|
+
return this.area(join(this.projectDir, 'cache'));
|
|
63
|
+
}
|
|
64
|
+
executions() {
|
|
65
|
+
return this.area(join(this.projectDir, 'executions'));
|
|
66
|
+
}
|
|
67
|
+
artifacts() {
|
|
68
|
+
return this.area(join(this.projectDir, 'artifacts'));
|
|
69
|
+
}
|
|
70
|
+
/** Learning is store-wide (provider/skill/workflow/plan outcomes apply across projects). */
|
|
71
|
+
learning() {
|
|
72
|
+
return this.area(join(this.home, 'learning'));
|
|
73
|
+
}
|
|
74
|
+
preferences() {
|
|
75
|
+
return this.area(join(this.userDir, 'preferences'));
|
|
76
|
+
}
|
|
77
|
+
/** Write the version stamp once (creates the project dir). No-op when stateless. */
|
|
78
|
+
stampMeta() {
|
|
79
|
+
if (!this.enabled || this.metaStamped)
|
|
80
|
+
return;
|
|
81
|
+
mkdirSync(this.projectDir, { recursive: true });
|
|
82
|
+
const metaPath = join(this.projectDir, 'meta.json');
|
|
83
|
+
if (!existsSync(metaPath))
|
|
84
|
+
writeFileSync(metaPath, JSON.stringify({ version: STORE_VERSION, projectId: this.projectId }));
|
|
85
|
+
this.metaStamped = true;
|
|
86
|
+
}
|
|
87
|
+
/** The stored version, or undefined when absent — a migration hook seam for future versions. */
|
|
88
|
+
storedVersion() {
|
|
89
|
+
const metaPath = join(this.projectDir, 'meta.json');
|
|
90
|
+
if (!existsSync(metaPath))
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(readFileSync(metaPath, 'utf8')).version;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Advisory per-project lock. Best-effort: acquires an exclusive lock file, steals a stale one, and
|
|
101
|
+
* otherwise proceeds anyway (advisory). Hard mutual exclusion for executions is a Phase 7 lease.
|
|
102
|
+
*/
|
|
103
|
+
withLock(fn, staleMs = 30_000) {
|
|
104
|
+
if (!this.enabled)
|
|
105
|
+
return fn();
|
|
106
|
+
mkdirSync(this.projectDir, { recursive: true });
|
|
107
|
+
const lockPath = join(this.projectDir, '.lock');
|
|
108
|
+
let held = false;
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
|
|
111
|
+
held = true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Held already — steal if stale.
|
|
115
|
+
try {
|
|
116
|
+
const age = this.clock.now() - statSync(lockPath).mtimeMs;
|
|
117
|
+
if (age > staleMs) {
|
|
118
|
+
writeFileSync(lockPath, String(process.pid));
|
|
119
|
+
held = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* proceed advisory */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return fn();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (held && existsSync(lockPath))
|
|
131
|
+
rmSync(lockPath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Aggregate integrity check across the project areas. */
|
|
135
|
+
check() {
|
|
136
|
+
if (!this.enabled)
|
|
137
|
+
return [];
|
|
138
|
+
return [...this.memory('project').check(), ...this.memory('user').check(), ...this.memory('repository').check(), ...this.conversations().check(), ...this.indexes().check()];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL telemetry sink. Metadata only, redacted before write. A telemetry write must
|
|
3
|
+
* NEVER fail a run, so all IO is wrapped and swallowed. The path should be outside the repo or
|
|
4
|
+
* gitignored (`init` adds `.ai-runtime/`).
|
|
5
|
+
*/
|
|
6
|
+
import type { TelemetrySink } from '../telemetry.js';
|
|
7
|
+
import type { TelemetryEvent } from '../../types.js';
|
|
8
|
+
export declare class FileSink implements TelemetrySink {
|
|
9
|
+
private readonly path;
|
|
10
|
+
constructor(path: string);
|
|
11
|
+
emit(event: TelemetryEvent): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL telemetry sink. Metadata only, redacted before write. A telemetry write must
|
|
3
|
+
* NEVER fail a run, so all IO is wrapped and swallowed. The path should be outside the repo or
|
|
4
|
+
* gitignored (`init` adds `.ai-runtime/`).
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { redact } from '../../security/redact.js';
|
|
9
|
+
export class FileSink {
|
|
10
|
+
path;
|
|
11
|
+
constructor(path) {
|
|
12
|
+
this.path = path;
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* ignore — a telemetry setup failure must not break the router */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
emit(event) {
|
|
21
|
+
try {
|
|
22
|
+
appendFileSync(this.path, `${JSON.stringify(redact(event))}\n`);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* a telemetry write must never fail a run */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry sinks. Events are ROUTING/PERF METADATA only — never prompt text, input, or keys — and
|
|
3
|
+
* are defensively passed through `redact()` before storage so a stray secret can never land in a sink.
|
|
4
|
+
*/
|
|
5
|
+
import type { TelemetryEvent } from '../types.js';
|
|
6
|
+
export interface TelemetrySink {
|
|
7
|
+
emit(event: TelemetryEvent): void;
|
|
8
|
+
events?(): TelemetryEvent[];
|
|
9
|
+
}
|
|
10
|
+
export declare class MemorySink implements TelemetrySink {
|
|
11
|
+
private readonly limit;
|
|
12
|
+
private readonly buffer;
|
|
13
|
+
constructor(limit?: number);
|
|
14
|
+
emit(event: TelemetryEvent): void;
|
|
15
|
+
events(): TelemetryEvent[];
|
|
16
|
+
}
|
|
17
|
+
export declare class NullSink implements TelemetrySink {
|
|
18
|
+
emit(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Centralized observability: forward each redacted event to a user callback — e.g. push to
|
|
22
|
+
* a collector, OpenTelemetry, or a log pipeline. Failures are swallowed so observability never breaks
|
|
23
|
+
* a run. Events are already metadata-only and redacted by the emitting sink chain.
|
|
24
|
+
*/
|
|
25
|
+
export declare class CallbackSink implements TelemetrySink {
|
|
26
|
+
private readonly forward;
|
|
27
|
+
constructor(forward: (event: TelemetryEvent) => void);
|
|
28
|
+
emit(event: TelemetryEvent): void;
|
|
29
|
+
}
|
|
30
|
+
/** Fan an event out to several sinks; `events()` reports the first queryable sink's buffer. */
|
|
31
|
+
export declare class MultiSink implements TelemetrySink {
|
|
32
|
+
private readonly sinks;
|
|
33
|
+
constructor(sinks: TelemetrySink[]);
|
|
34
|
+
emit(event: TelemetryEvent): void;
|
|
35
|
+
events(): TelemetryEvent[];
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry sinks. Events are ROUTING/PERF METADATA only — never prompt text, input, or keys — and
|
|
3
|
+
* are defensively passed through `redact()` before storage so a stray secret can never land in a sink.
|
|
4
|
+
*/
|
|
5
|
+
import { redact } from '../security/redact.js';
|
|
6
|
+
export class MemorySink {
|
|
7
|
+
limit;
|
|
8
|
+
buffer = [];
|
|
9
|
+
constructor(limit = 1000) {
|
|
10
|
+
this.limit = limit;
|
|
11
|
+
}
|
|
12
|
+
emit(event) {
|
|
13
|
+
this.buffer.push(redact(event));
|
|
14
|
+
if (this.buffer.length > this.limit)
|
|
15
|
+
this.buffer.shift();
|
|
16
|
+
}
|
|
17
|
+
events() {
|
|
18
|
+
return [...this.buffer];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class NullSink {
|
|
22
|
+
emit() {
|
|
23
|
+
/* no-op */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Centralized observability: forward each redacted event to a user callback — e.g. push to
|
|
28
|
+
* a collector, OpenTelemetry, or a log pipeline. Failures are swallowed so observability never breaks
|
|
29
|
+
* a run. Events are already metadata-only and redacted by the emitting sink chain.
|
|
30
|
+
*/
|
|
31
|
+
export class CallbackSink {
|
|
32
|
+
forward;
|
|
33
|
+
constructor(forward) {
|
|
34
|
+
this.forward = forward;
|
|
35
|
+
}
|
|
36
|
+
emit(event) {
|
|
37
|
+
try {
|
|
38
|
+
this.forward(redact(event));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* observability must never break a run */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Fan an event out to several sinks; `events()` reports the first queryable sink's buffer. */
|
|
46
|
+
export class MultiSink {
|
|
47
|
+
sinks;
|
|
48
|
+
constructor(sinks) {
|
|
49
|
+
this.sinks = sinks;
|
|
50
|
+
}
|
|
51
|
+
emit(event) {
|
|
52
|
+
for (const sink of this.sinks)
|
|
53
|
+
sink.emit(event);
|
|
54
|
+
}
|
|
55
|
+
events() {
|
|
56
|
+
for (const sink of this.sinks) {
|
|
57
|
+
const ev = sink.events?.();
|
|
58
|
+
if (ev)
|
|
59
|
+
return ev;
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem tool — read/write/list/exists, strictly jailed to the workspace. Reads are gated on
|
|
3
|
+
* `filesystem.read`; writes on `filesystem.write` (or interactive approval). Every path is resolved
|
|
4
|
+
* through the jail, so `..`, absolute escapes, and symlink/junction/UNC escapes are rejected.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from '../tool.js';
|
|
7
|
+
export declare const filesystemTool: Tool;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem tool — read/write/list/exists, strictly jailed to the workspace. Reads are gated on
|
|
3
|
+
* `filesystem.read`; writes on `filesystem.write` (or interactive approval). Every path is resolved
|
|
4
|
+
* through the jail, so `..`, absolute escapes, and symlink/junction/UNC escapes are rejected.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { authorize, denied } from '../tool.js';
|
|
9
|
+
import { resolveInJail, JailError } from '../jail.js';
|
|
10
|
+
export const filesystemTool = {
|
|
11
|
+
id: 'filesystem',
|
|
12
|
+
description: 'Read, write, list, and check files within the workspace (jailed; no escape).',
|
|
13
|
+
parameters: { op: 'read | write | list | exists', path: 'string (relative to workspace)', content: 'string (write only)' },
|
|
14
|
+
async execute(input, ctx) {
|
|
15
|
+
const { op, path, content } = (input ?? {});
|
|
16
|
+
if (!op || typeof path !== 'string' || path === '')
|
|
17
|
+
return denied('INVALID_INPUT', 'filesystem requires { op, path }');
|
|
18
|
+
let abs;
|
|
19
|
+
try {
|
|
20
|
+
abs = resolveInJail(ctx.workspaceRoot, path);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e instanceof JailError)
|
|
24
|
+
return denied('JAILBREAK', e.message);
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
if (op === 'read' || op === 'list' || op === 'exists') {
|
|
28
|
+
if (!ctx.permissions.filesystem.read)
|
|
29
|
+
return denied('PERMISSION', 'filesystem read is not permitted');
|
|
30
|
+
if (op === 'exists')
|
|
31
|
+
return { ok: true, data: existsSync(abs) };
|
|
32
|
+
if (!existsSync(abs))
|
|
33
|
+
return denied('NOT_FOUND', `no such path: ${path}`);
|
|
34
|
+
if (op === 'list') {
|
|
35
|
+
if (!statSync(abs).isDirectory())
|
|
36
|
+
return denied('INVALID_INPUT', `not a directory: ${path}`);
|
|
37
|
+
const entries = readdirSync(abs);
|
|
38
|
+
return { ok: true, data: entries, output: entries.join('\n') };
|
|
39
|
+
}
|
|
40
|
+
const text = readFileSync(abs, 'utf8');
|
|
41
|
+
return { ok: true, output: text, data: text };
|
|
42
|
+
}
|
|
43
|
+
if (op === 'write') {
|
|
44
|
+
const allowed = await authorize(ctx, ctx.permissions.filesystem.write, { action: `write file ${path}`, risk: 'medium' });
|
|
45
|
+
if (!allowed)
|
|
46
|
+
return denied('PERMISSION', `filesystem write is not permitted: ${path}`);
|
|
47
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
48
|
+
writeFileSync(abs, content ?? '');
|
|
49
|
+
return { ok: true, output: `wrote ${path}` };
|
|
50
|
+
}
|
|
51
|
+
return denied('INVALID_INPUT', `unknown filesystem op: ${String(op)}`);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git tool. Reads (status/diff/log) are always allowed. Mutations are gated: commit on `git.commit`,
|
|
3
|
+
* push on `git.push` (or interactive approval). `status` reports whether the working tree is dirty so a
|
|
4
|
+
* caller can confirm before the first mutating step. Uses the injectable runner (hermetic in tests).
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from '../tool.js';
|
|
7
|
+
import type { CommandRunner } from '../runner.js';
|
|
8
|
+
export declare function createGitTool(runner?: CommandRunner): Tool;
|
|
9
|
+
/** The git tool with the default (real) runner. */
|
|
10
|
+
export declare const gitTool: Tool;
|