@botbotgo/runtime 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.github/workflows/release.yml +63 -0
  2. package/config/examples/runtime.yaml +14 -0
  3. package/config/examples/tool.yaml +1 -1
  4. package/dist/config/resolveRuntimeConfig.d.ts +3 -1
  5. package/dist/config/resolveRuntimeConfig.d.ts.map +1 -1
  6. package/dist/config/resolveRuntimeConfig.js +2 -0
  7. package/dist/config/resolveRuntimeConfig.js.map +1 -1
  8. package/dist/config/resources.d.ts +17 -0
  9. package/dist/config/resources.d.ts.map +1 -1
  10. package/dist/config/resources.js.map +1 -1
  11. package/dist/runtime/bootstrap/runtimeFactory.d.ts.map +1 -1
  12. package/dist/runtime/bootstrap/runtimeFactory.js +4 -0
  13. package/dist/runtime/bootstrap/runtimeFactory.js.map +1 -1
  14. package/dist/runtime/execution/agentRunExecutor.d.ts +1 -0
  15. package/dist/runtime/execution/agentRunExecutor.d.ts.map +1 -1
  16. package/dist/runtime/execution/agentRunExecutor.js +3 -0
  17. package/dist/runtime/execution/agentRunExecutor.js.map +1 -1
  18. package/dist/runtime/execution/agentRunExecutor.types.d.ts +2 -0
  19. package/dist/runtime/execution/agentRunExecutor.types.d.ts.map +1 -1
  20. package/dist/runtime/middleware/agentToolMiddleware.d.ts +2 -0
  21. package/dist/runtime/middleware/agentToolMiddleware.d.ts.map +1 -1
  22. package/dist/runtime/middleware/agentToolMiddleware.js +17 -4
  23. package/dist/runtime/middleware/agentToolMiddleware.js.map +1 -1
  24. package/dist/runtime/middleware/commandPolicy.d.ts +2 -1
  25. package/dist/runtime/middleware/commandPolicy.d.ts.map +1 -1
  26. package/dist/runtime/middleware/commandPolicy.js +14 -11
  27. package/dist/runtime/middleware/commandPolicy.js.map +1 -1
  28. package/dist/runtime/middleware/frameworkPrompt.d.ts.map +1 -1
  29. package/dist/runtime/middleware/frameworkPrompt.js +2 -3
  30. package/dist/runtime/middleware/frameworkPrompt.js.map +1 -1
  31. package/dist/runtime/middleware/toolArgsNormalizer.d.ts +1 -0
  32. package/dist/runtime/middleware/toolArgsNormalizer.d.ts.map +1 -1
  33. package/dist/runtime/middleware/toolArgsNormalizer.js +32 -0
  34. package/dist/runtime/middleware/toolArgsNormalizer.js.map +1 -1
  35. package/dist/runtime/middleware/toolCallGuard.d.ts +3 -1
  36. package/dist/runtime/middleware/toolCallGuard.d.ts.map +1 -1
  37. package/dist/runtime/middleware/toolCallGuard.js +24 -4
  38. package/dist/runtime/middleware/toolCallGuard.js.map +1 -1
  39. package/dist/runtime/middleware/types.d.ts +2 -0
  40. package/dist/runtime/middleware/types.d.ts.map +1 -1
  41. package/dist/runtime/middleware/types.js.map +1 -1
  42. package/dist/runtime/runtimeService.d.ts +2 -0
  43. package/dist/runtime/runtimeService.d.ts.map +1 -1
  44. package/dist/runtime/runtimeService.js +1 -0
  45. package/dist/runtime/runtimeService.js.map +1 -1
  46. package/dist/runtime/stream/runArtifacts.d.ts.map +1 -1
  47. package/dist/runtime/stream/runArtifacts.js +3 -1
  48. package/dist/runtime/stream/runArtifacts.js.map +1 -1
  49. package/dist/state/runState.d.ts +1 -0
  50. package/dist/state/runState.d.ts.map +1 -1
  51. package/dist/state/runState.js +18 -1
  52. package/dist/state/runState.js.map +1 -1
  53. package/dist/state/workspaceState.d.ts +2 -0
  54. package/dist/state/workspaceState.d.ts.map +1 -1
  55. package/dist/state/workspaceState.js +12 -10
  56. package/dist/state/workspaceState.js.map +1 -1
  57. package/example/config/model.yaml +2 -2
  58. package/example/config/runtime.yaml +19 -1
  59. package/example/package.json +0 -1
  60. package/package.json +1 -1
  61. package/src/config/resolveRuntimeConfig.ts +5 -0
  62. package/src/config/resources.ts +19 -0
  63. package/src/runtime/bootstrap/runtimeFactory.ts +7 -0
  64. package/src/runtime/execution/agentRunExecutor.ts +3 -0
  65. package/src/runtime/execution/agentRunExecutor.types.ts +2 -0
  66. package/src/runtime/middleware/agentToolMiddleware.ts +19 -3
  67. package/src/runtime/middleware/commandPolicy.ts +22 -10
  68. package/src/runtime/middleware/frameworkPrompt.ts +2 -3
  69. package/src/runtime/middleware/toolArgsNormalizer.ts +36 -0
  70. package/src/runtime/middleware/toolCallGuard.ts +37 -3
  71. package/src/runtime/middleware/types.ts +2 -0
  72. package/src/runtime/runtimeService.ts +3 -0
  73. package/src/runtime/stream/runArtifacts.ts +3 -1
  74. package/src/state/runState.ts +19 -1
  75. package/src/state/workspaceState.ts +19 -11
  76. package/test/unit/config/loader.test.ts +10 -0
  77. package/test/unit/runtime/agentToolMiddleware.test.ts +51 -0
  78. package/test/unit/runtime/toolArgsNormalizer.test.ts +34 -0
  79. package/test/unit/runtime/toolCallGuard.test.ts +71 -0
  80. package/test/unit/runtime/workspaceState.test.ts +94 -0
  81. package/example/.tsbuildinfo +0 -1
  82. package/example/build/.tsbuildinfo +0 -1
  83. package/example/serve-output.mjs +0 -52
@@ -20,7 +20,12 @@ export interface AgentWorkspaceState {
20
20
  todosFile: string;
21
21
  runStateFile: string;
22
22
  artifactFiles: Record<string, string>;
23
- prepareRun(params: { prompt: string; fallbackThreadId?: string; resumeHint: string }): Promise<PreparedWorkspaceRun>;
23
+ prepareRun(params: {
24
+ prompt: string;
25
+ resumeInterruptedRun?: boolean;
26
+ fallbackThreadId?: string;
27
+ resumeHint: string;
28
+ }): Promise<PreparedWorkspaceRun>;
24
29
  readRunState(): Promise<AgentRunState | null>;
25
30
  getThreadId(previousState: AgentRunState | null, fallbackThreadId: string): string;
26
31
  buildResumePrompt(basePrompt: string, previousState: AgentRunState | null, resumeHint: string): string;
@@ -89,22 +94,24 @@ export class AgentWorkspaceStateManager implements AgentWorkspaceState {
89
94
 
90
95
  public async prepareRun(params: {
91
96
  prompt: string;
97
+ resumeInterruptedRun?: boolean;
92
98
  fallbackThreadId?: string;
93
99
  resumeHint: string;
94
100
  }): Promise<PreparedWorkspaceRun> {
95
101
  const previousState = await this.runState.read();
96
- const threadId = this.runState.getThreadId(previousState, params.fallbackThreadId ?? `t-${Date.now()}`);
102
+ const effectivePreviousState = params.resumeInterruptedRun ? previousState : null;
103
+ const threadId = this.runState.getThreadId(effectivePreviousState, params.fallbackThreadId ?? `t-${Date.now()}`);
97
104
  this.events?.emit({
98
105
  name: "agent.runtime2.workspace.prepare",
99
106
  from: "agent-runtime2.runtime",
100
107
  to: "agent-runtime2.workspace",
101
- payload: { threadId, previousState },
108
+ payload: { threadId, previousState: effectivePreviousState },
102
109
  });
103
- await this.resetForFreshRun(previousState);
110
+ await this.resetForFreshRun(effectivePreviousState);
104
111
  return WorkspaceRunSessionFactory.create({
105
112
  prompt: params.prompt,
106
113
  threadId,
107
- previousState,
114
+ previousState: effectivePreviousState,
108
115
  resumeHint: params.resumeHint,
109
116
  runState: this.runState,
110
117
  events: this.events,
@@ -176,13 +183,14 @@ export class AgentWorkspaceStateManager implements AgentWorkspaceState {
176
183
  }
177
184
 
178
185
  public async resetForFreshRun(previousState: AgentRunState | null): Promise<void> {
179
- if (previousState?.status !== "running") return;
180
- if (!this.legacyOutputState) {
181
- await Promise.all([
182
- unlink(this.legacyTodosFile).catch(() => {}),
186
+ if (previousState?.status === "running") return;
187
+ await Promise.all([
188
+ unlink(this.todosFile).catch(() => {}),
189
+ unlink(this.legacyTodosFile).catch(() => {}),
190
+ ...(!this.legacyOutputState ? [
183
191
  unlink(this.legacyRunStateFile).catch(() => {}),
184
- ]);
185
- }
192
+ ] : []),
193
+ ]);
186
194
  }
187
195
 
188
196
  public async persistTodos(todos: unknown): Promise<void> {
@@ -95,6 +95,12 @@ spec:
95
95
  malformedToolCallMaxRetries: 1
96
96
  idleTimeoutMaxRetries: 1
97
97
  heartbeatIntervalMs: 10000
98
+ middleware:
99
+ forbidScriptSourceRead: true
100
+ forbidAbsolutePathsOutsideWorkspace: true
101
+ commandPolicy:
102
+ blockDirectNetworkFetch: true
103
+ blockShellDashLc: true
98
104
  debug:
99
105
  run: true
100
106
  workspace: true
@@ -130,6 +136,8 @@ describe("config/loader", () => {
130
136
  assert.strictEqual(r.spec.systemPrompt, "You are a runtime test agent.");
131
137
  assert.strictEqual(r.spec.backend?.type, "local_shell");
132
138
  assert.strictEqual(r.spec.malformedToolCallMaxRetries, 1);
139
+ assert.strictEqual(r.spec.middleware?.forbidScriptSourceRead, true);
140
+ assert.strictEqual(r.spec.middleware?.commandPolicy?.blockDirectNetworkFetch, true);
133
141
  assert.strictEqual(r.spec.debug?.toolCall, true);
134
142
  assert.strictEqual(r.spec.debug?.stream, false);
135
143
  assert.strictEqual(r.spec.eventLogLevel, "tools");
@@ -331,6 +339,8 @@ describe("deepagents", () => {
331
339
  assert.strictEqual(resolved.runtime.systemPrompt, "You are a runtime test agent.");
332
340
  assert.strictEqual(resolved.runtime.llmIdleTimeoutMs, 300000);
333
341
  assert.strictEqual(resolved.runtime.heartbeatIntervalMs, 10000);
342
+ assert.strictEqual(resolved.runtime.middleware?.forbidScriptSourceRead, true);
343
+ assert.strictEqual(resolved.runtime.middleware?.commandPolicy?.blockShellDashLc, true);
334
344
  assert.strictEqual(resolved.runtime.debug?.run, true);
335
345
  assert.strictEqual(resolved.runtime.debug?.stream, false);
336
346
  assert.strictEqual(resolved.runtime.eventLogLevel, "tools");
@@ -0,0 +1,51 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { AgentToolMiddlewareFactory } from "../../../src/runtime/middleware/agentToolMiddleware.ts";
4
+
5
+ class FakeToolMessage {
6
+ public readonly content: string;
7
+ public readonly tool_call_id: string;
8
+
9
+ public constructor(fields: { content: string; tool_call_id: string }) {
10
+ this.content = fields.content;
11
+ this.tool_call_id = fields.tool_call_id;
12
+ }
13
+ }
14
+
15
+ test("AgentToolMiddleware emits blocked error details for policy violations", async () => {
16
+ const events: Array<{ name: string; payload?: Record<string, unknown> }> = [];
17
+ const middleware = new AgentToolMiddlewareFactory({
18
+ rootDir: "/workspace",
19
+ createMiddleware: (definition) => definition,
20
+ ToolMessage: FakeToolMessage,
21
+ events: {
22
+ emit: (event) => {
23
+ events.push(event);
24
+ },
25
+ },
26
+ }).create() as {
27
+ wrapToolCall: (request: unknown, handler: (request: unknown) => unknown) => Promise<unknown>;
28
+ };
29
+
30
+ const result = await middleware.wrapToolCall(
31
+ {
32
+ toolCall: {
33
+ id: "call-1",
34
+ name: "read_file",
35
+ args: { path: "/tmp/outside-workspace.sh" },
36
+ },
37
+ threadId: "thread-1",
38
+ },
39
+ async () => {
40
+ throw new Error("handler should not be called");
41
+ },
42
+ );
43
+
44
+ assert.ok(result instanceof FakeToolMessage);
45
+ assert.match(result.content, /Do not use absolute filesystem paths outside workspace root/);
46
+
47
+ const blocked = events.find((event) => event.name === "agent.runtime2.tool.call.blocked");
48
+ assert.ok(blocked);
49
+ assert.equal(blocked.payload?.reason, "policy_violation");
50
+ assert.match(String(blocked.payload?.error ?? ""), /Do not use absolute filesystem paths outside workspace root/);
51
+ });
@@ -76,6 +76,7 @@ test("FrameworkPrompt includes active skill mapping in framework system prompt",
76
76
  assert.match(prompt, /other-skill => \/cache\/skills\/other-skill/);
77
77
  assert.match(prompt, /\$\{SKILL_PATH:<skill-id>\}/);
78
78
  assert.match(prompt, /\$\{WORKSPACE\}/);
79
+ assert.doesNotMatch(prompt, /\bls\b|\bglob\b/);
79
80
  });
80
81
 
81
82
  test("buildSkillPathMap expands skills root directories into named skill paths", async () => {
@@ -134,3 +135,36 @@ test("ToolArgsNormalizer expands unnamed SKILL_PATH to the only active skill dir
134
135
 
135
136
  await rm(root, { recursive: true, force: true });
136
137
  });
138
+
139
+ test("ToolArgsNormalizer strips workspace path prefixes from mistaken repo-relative file and command paths", async () => {
140
+ const root = join(tmpdir(), `agent-runtime2-workspace-paths-${Date.now()}`, "framework", "runtime", "example");
141
+ const skillRoot = join(root, ".agent", "cache", "skills", "company-report");
142
+ await mkdir(join(skillRoot, "scripts"), { recursive: true });
143
+ await writeFile(join(skillRoot, "scripts", "merge-report.mjs"), "export {};\n", "utf8");
144
+
145
+ const normalizedWrite = ToolArgsNormalizer.normalizeToolArgs(
146
+ root,
147
+ [skillRoot],
148
+ "write_file",
149
+ {
150
+ file_path: "runtime/example/output/company-report.json",
151
+ content: "{}",
152
+ },
153
+ );
154
+ assert.equal(normalizedWrite.file_path, "output/company-report.json");
155
+
156
+ const normalizedExecute = ToolArgsNormalizer.normalizeToolArgs(
157
+ root,
158
+ [skillRoot],
159
+ "execute",
160
+ {
161
+ command: "node scripts/merge-report.mjs runtime/example/output/company-report.json runtime/example/output/company-report.html",
162
+ },
163
+ );
164
+ assert.equal(
165
+ normalizedExecute.command,
166
+ "node .agent/cache/skills/company-report/scripts/merge-report.mjs output/company-report.json output/company-report.html",
167
+ );
168
+
169
+ await rm(root, { recursive: true, force: true });
170
+ });
@@ -0,0 +1,71 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ToolCallGuard } from "../../../src/runtime/middleware/toolCallGuard.ts";
4
+
5
+ class FakeToolMessage {
6
+ public readonly content: string;
7
+ public readonly tool_call_id: string;
8
+
9
+ public constructor(fields: { content: string; tool_call_id: string }) {
10
+ this.content = fields.content;
11
+ this.tool_call_id = fields.tool_call_id;
12
+ }
13
+ }
14
+
15
+ test("ToolCallGuard blocks script source reads by default", () => {
16
+ const result = ToolCallGuard.maybeBlock(
17
+ "read_file",
18
+ { path: "skills/company-report/scripts/merge-report.mjs" },
19
+ "call-1",
20
+ FakeToolMessage,
21
+ "/workspace",
22
+ );
23
+
24
+ assert.ok(result instanceof FakeToolMessage);
25
+ assert.match(result.content, /Do not inspect script source files/);
26
+ });
27
+
28
+ test("ToolCallGuard respects runtime middleware policy overrides", () => {
29
+ const readAllowed = ToolCallGuard.maybeBlock(
30
+ "read_file",
31
+ { path: "skills/company-report/scripts/merge-report.mjs" },
32
+ "call-2",
33
+ FakeToolMessage,
34
+ "/workspace",
35
+ undefined,
36
+ [],
37
+ {
38
+ forbidScriptSourceRead: false,
39
+ },
40
+ );
41
+ assert.equal(readAllowed, null);
42
+
43
+ const executeAllowed = ToolCallGuard.maybeBlock(
44
+ "execute",
45
+ { command: "curl https://example.com/data.json" },
46
+ "call-3",
47
+ FakeToolMessage,
48
+ "/workspace",
49
+ undefined,
50
+ [],
51
+ {
52
+ commandPolicy: {
53
+ blockDirectNetworkFetch: false,
54
+ },
55
+ },
56
+ );
57
+ assert.equal(executeAllowed, null);
58
+ });
59
+
60
+ test("ToolCallGuard blocks reads of internal runtime state files", () => {
61
+ const result = ToolCallGuard.maybeBlock(
62
+ "read_file",
63
+ { path: ".agent/run-state.json" },
64
+ "call-4",
65
+ FakeToolMessage,
66
+ "/workspace",
67
+ );
68
+
69
+ assert.ok(result instanceof FakeToolMessage);
70
+ assert.match(result.content, /Do not inspect internal runtime state files under \.agent\//);
71
+ });
@@ -0,0 +1,94 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createAgentWorkspaceState } from "../../../src/state/workspaceState.ts";
7
+
8
+ test("workspace prepareRun starts fresh when resumeInterruptedRun is disabled", async () => {
9
+ const rootDir = join(tmpdir(), `agent-runtime2-workspace-${Date.now()}`);
10
+ const outputDir = join(rootDir, "output");
11
+ const agentDir = join(rootDir, ".agent");
12
+ await mkdir(outputDir, { recursive: true });
13
+ await mkdir(agentDir, { recursive: true });
14
+ await writeFile(
15
+ join(agentDir, "run-state.json"),
16
+ JSON.stringify({
17
+ prompt: "old",
18
+ threadId: "t-old",
19
+ status: "running",
20
+ startedAt: new Date().toISOString(),
21
+ updatedAt: new Date().toISOString(),
22
+ stepCount: 3,
23
+ lastSummary: "old summary",
24
+ artifacts: {},
25
+ }),
26
+ "utf8",
27
+ );
28
+ await writeFile(join(agentDir, "todos.json"), "[]\n", "utf8");
29
+ await writeFile(join(outputDir, "company-report.json"), "{\"stale\":true}\n", "utf8");
30
+
31
+ const workspace = createAgentWorkspaceState({
32
+ rootDir,
33
+ artifacts: [{ key: "companyReport", path: "output/company-report.json" }],
34
+ });
35
+ const prepared = await workspace.prepareRun({
36
+ prompt: "new prompt",
37
+ resumeInterruptedRun: false,
38
+ resumeHint: "resume",
39
+ });
40
+
41
+ assert.notEqual(prepared.threadId, "t-old");
42
+ assert.equal(prepared.input.messages[0]?.content, "new prompt");
43
+ await assert.rejects(readFile(join(agentDir, "todos.json"), "utf8"));
44
+
45
+ await rm(rootDir, { recursive: true, force: true });
46
+ });
47
+
48
+ test("workspace prepareRun can explicitly resume an interrupted run with checkpoint context", async () => {
49
+ const rootDir = join(tmpdir(), `agent-runtime2-workspace-resume-${Date.now()}`);
50
+ const outputDir = join(rootDir, "output");
51
+ const agentDir = join(rootDir, ".agent");
52
+ await mkdir(outputDir, { recursive: true });
53
+ await mkdir(agentDir, { recursive: true });
54
+ await writeFile(
55
+ join(agentDir, "run-state.json"),
56
+ JSON.stringify({
57
+ prompt: "old",
58
+ threadId: "t-old",
59
+ status: "running",
60
+ startedAt: new Date().toISOString(),
61
+ updatedAt: new Date().toISOString(),
62
+ stepCount: 3,
63
+ lastSummary: "Tool call: yahooFinanceNews",
64
+ artifacts: {
65
+ companyReportPresent: true,
66
+ companyReportHtmlPresent: false,
67
+ },
68
+ }),
69
+ "utf8",
70
+ );
71
+
72
+ const workspace = createAgentWorkspaceState({
73
+ rootDir,
74
+ artifacts: [
75
+ { key: "companyReport", path: "output/company-report.json" },
76
+ { key: "companyReportHtml", path: "output/company-report.html" },
77
+ ],
78
+ });
79
+ const prepared = await workspace.prepareRun({
80
+ prompt: "new prompt",
81
+ resumeInterruptedRun: true,
82
+ resumeHint: "Continue from the checkpoint instead of restarting.",
83
+ });
84
+
85
+ assert.equal(prepared.threadId, "t-old");
86
+ assert.match(prepared.input.messages[0]?.content ?? "", /RESUME_CHECKPOINT/);
87
+ assert.match(prepared.input.messages[0]?.content ?? "", /"threadId": "t-old"/);
88
+ assert.match(prepared.input.messages[0]?.content ?? "", /"lastSummary": "Tool call: yahooFinanceNews"/);
89
+ assert.match(prepared.input.messages[0]?.content ?? "", /"companyReport": "ready"/);
90
+ assert.match(prepared.input.messages[0]?.content ?? "", /"companyReportHtml": "missing"/);
91
+ assert.match(prepared.input.messages[0]?.content ?? "", /Do not read \.agent\/\*/);
92
+
93
+ await rm(rootDir, { recursive: true, force: true });
94
+ });