@realqiyan/agent-task 0.0.1-rc.1 → 0.0.1-rc.3

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 (91) hide show
  1. package/README.md +8 -5
  2. package/bin/agent-task.mjs +23 -41
  3. package/dist/application/launcher.js +215 -0
  4. package/dist/application/main.js +122 -0
  5. package/dist/application/startup.js +51 -0
  6. package/dist/application/task-runtime.js +42 -0
  7. package/dist/configuration/contract.js +28 -0
  8. package/dist/configuration/environment.js +32 -0
  9. package/dist/configuration/files.js +43 -0
  10. package/dist/configuration/management.js +6 -0
  11. package/dist/configuration/model.js +2 -0
  12. package/dist/configuration/policy.js +29 -0
  13. package/dist/configuration/ports.js +2 -0
  14. package/dist/{settings.js → configuration/settings.js} +97 -42
  15. package/dist/executors/environment.js +10 -0
  16. package/dist/executors/model.js +2 -0
  17. package/dist/{permissions.js → executors/permissions.js} +3 -4
  18. package/dist/executors/registry.js +29 -0
  19. package/dist/executors/validation.js +10 -0
  20. package/dist/{lock.js → infrastructure/lock.js} +17 -1
  21. package/dist/{private-files.js → infrastructure/private-files.js} +17 -9
  22. package/dist/infrastructure/sqlite.js +19 -0
  23. package/dist/infrastructure/version.js +3 -0
  24. package/dist/{acp.js → integrations/acp/connection.js} +14 -11
  25. package/dist/{node-execution.js → integrations/acp/execution.js} +3 -4
  26. package/dist/integrations/acp/runtime.js +28 -0
  27. package/dist/{worker.js → integrations/acp/worker.js} +5 -4
  28. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  29. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  30. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  31. package/dist/integrations/local-service/control.js +33 -0
  32. package/dist/protocol/cli/admin-client.js +45 -0
  33. package/dist/protocol/cli/commands.js +394 -0
  34. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  35. package/dist/protocol/http/server.js +111 -0
  36. package/dist/{web.js → protocol/http/web.js} +31 -18
  37. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  38. package/dist/protocol/services.js +2 -0
  39. package/dist/protocol/settings-contract.js +40 -0
  40. package/dist/protocol/task-contract.js +28 -0
  41. package/dist/results/files.js +70 -0
  42. package/dist/results/ports.js +2 -0
  43. package/dist/results/service.js +42 -0
  44. package/dist/shared/errors.js +13 -0
  45. package/dist/shared/limits.js +3 -0
  46. package/dist/shared/time.js +2 -0
  47. package/dist/shared/validation.js +8 -0
  48. package/dist/tasks/definitions.js +83 -0
  49. package/dist/tasks/graph.js +23 -0
  50. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  51. package/dist/tasks/model.js +2 -0
  52. package/dist/tasks/ports.js +2 -0
  53. package/dist/tasks/runs.js +263 -0
  54. package/dist/tasks/schedule.js +36 -0
  55. package/dist/tasks/sessions.js +62 -0
  56. package/dist/tasks/storage/records.js +45 -0
  57. package/dist/{task-storage.js → tasks/storage/store.js} +23 -36
  58. package/dist/tasks/supervisor.js +227 -0
  59. package/dist/tasks/validation.js +24 -0
  60. package/dist/web/api.js +1 -1
  61. package/dist/web/app.js +7 -0
  62. package/dist/web/settings-view.js +40 -28
  63. package/docs/architecture-guidelines.md +8 -6
  64. package/docs/cli.md +49 -0
  65. package/docs/deployment.md +32 -70
  66. package/docs/design-proposal.md +3 -3
  67. package/docs/release.md +17 -18
  68. package/docs/technical-design.md +47 -26
  69. package/docs/validation.md +57 -17
  70. package/docs/web-console.md +15 -19
  71. package/package.json +8 -8
  72. package/scripts/install-lib.mjs +7 -105
  73. package/scripts/mcp-call.mjs +1 -1
  74. package/scripts/probe-agent.mjs +11 -5
  75. package/scripts/service-runner.mjs +2 -7
  76. package/scripts/setup-wizard.mjs +3 -8
  77. package/scripts/setup.mjs +31 -40
  78. package/deploy/agent-task.service.example +0 -17
  79. package/dist/admin.js +0 -31
  80. package/dist/config.js +0 -113
  81. package/dist/environment.js +0 -23
  82. package/dist/main.js +0 -131
  83. package/dist/model.js +0 -28
  84. package/dist/results.js +0 -85
  85. package/dist/settings-contract.js +0 -16
  86. package/dist/task-contract.js +0 -37
  87. package/dist/task-model.js +0 -2
  88. package/dist/tasks.js +0 -509
  89. package/dist/version.js +0 -3
  90. package/scripts/uninstall.mjs +0 -53
  91. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
@@ -1,9 +1,11 @@
1
1
  import express from "express";
2
2
  import { fileURLToPath } from "node:url";
3
- import { bearerVerifier } from "./http-auth.js";
4
- import { SettingsError } from "./settings.js";
5
- import { StorageError } from "./task-storage.js";
6
- import { taskIdSchema, taskListSchema, taskRunListSchema, taskUpdateSchema } from "./task-contract.js";
3
+ import { bearerVerifier } from "./auth.js";
4
+ import { SettingsError } from "../../configuration/settings.js";
5
+ import { previewSettings } from "../../configuration/management.js";
6
+ import { StorageError } from "../../shared/errors.js";
7
+ import { parseSettings, settingsConfigSchema, settingsChangeSchema, credentialUpdateSchema } from "../settings-contract.js";
8
+ import { taskIdSchema, taskListSchema, taskRunListSchema, taskUpdateSchema } from "../task-contract.js";
7
9
  function query(schema, request) {
8
10
  const value = { ...request.query };
9
11
  for (const key of ["after", "limit"]) {
@@ -16,7 +18,7 @@ function query(schema, request) {
16
18
  }
17
19
  return schema.parse(value);
18
20
  }
19
- export function createWebRouter(tasks, settings, token, fatal) {
21
+ export function createWebRouter(tasks, settings, token, fatal, restart, instanceId) {
20
22
  const router = express.Router(), authorized = bearerVerifier(token);
21
23
  router.use((req, res, next) => {
22
24
  res.set({
@@ -53,7 +55,7 @@ export function createWebRouter(tasks, settings, token, fatal) {
53
55
  next();
54
56
  });
55
57
  const assets = new Map(["index.html", "app.js", "api.js", "dom.js", "tasks-view.js", "settings-view.js", "style.css"]
56
- .map(name => [name === "index.html" ? "/" : "/" + name, fileURLToPath(new URL("./web/" + name, import.meta.url))]));
58
+ .map(name => [name === "index.html" ? "/" : "/" + name, fileURLToPath(new URL("../../web/" + name, import.meta.url))]));
57
59
  router.get(["/", ...[...assets.keys()].filter(path => path !== "/")], (req, res) => {
58
60
  res.sendFile(assets.get(req.path), { lastModified: false, cacheControl: false, acceptRanges: false }, error => { if (error && !res.headersSent)
59
61
  res.status(404).end(); });
@@ -75,27 +77,38 @@ export function createWebRouter(tasks, settings, token, fatal) {
75
77
  res.status(400).json({ error: "Request rejected; check inputs and refresh the current state" });
76
78
  }
77
79
  };
78
- router.get("/api/status", respond(() => ({ ...tasks.status(), settings: settings.status() })));
80
+ router.get("/api/status", respond(() => ({ ...tasks.supervisor.status(), settings: settings.status(), instanceId })));
79
81
  router.get("/api/tasks", respond(req => {
80
- const input = query(taskListSchema, req), page = tasks.list(input.after, input.limit);
82
+ const input = query(taskListSchema, req), page = tasks.definitions.list(input.after, input.limit);
81
83
  return { ...page, items: page.items.map(task => ({ id: task.id, name: task.input.name, status: task.status, nextAt: task.nextAt, schedule: task.input.schedule })) };
82
84
  }));
83
- router.get("/api/tasks/:id", respond(req => tasks.get(taskIdSchema.parse(req.params).id)));
85
+ router.get("/api/tasks/:id", respond(req => tasks.definitions.get(taskIdSchema.parse(req.params).id)));
84
86
  router.get("/api/runs", respond(req => {
85
- const input = query(taskRunListSchema, req), page = tasks.runs(input.taskId, input.after, input.limit);
87
+ const input = query(taskRunListSchema, req), page = tasks.runs.list(input.taskId, input.after, input.limit);
86
88
  return { ...page, items: page.items.map(run => ({ id: run.id, taskId: run.taskId, status: run.status,
87
89
  createdAt: run.createdAt, finishedAt: run.finishedAt, error: run.error,
88
90
  accepted: run.nodes.filter(node => node.status === "succeeded").length, total: run.nodes.length })) };
89
91
  }));
90
- router.get("/api/runs/:id", respond(req => tasks.run(taskIdSchema.parse(req.params).id)));
91
- router.post("/api/task-update", respond(req => tasks.update(taskUpdateSchema.parse(req.body))));
92
+ router.get("/api/runs/:id", respond(req => tasks.runs.get(taskIdSchema.parse(req.params).id)));
93
+ router.post("/api/task-update", respond(req => tasks.definitions.update(taskUpdateSchema.parse(req.body))));
92
94
  router.get("/api/settings", respond(() => settings.get()));
93
- router.post("/api/settings/validate", respond(req => {
94
- const config = settings.validate(req.body);
95
- return { config, affectedTasks: tasks.executorImpact(config.executors) };
96
- }));
97
- router.post("/api/settings/config", respond(req => settings.saveConfig(req.body)));
98
- router.post("/api/settings/credential", respond(req => settings.saveCredential(req.body)));
95
+ router.get("/api/settings/diff", respond(() => settings.diff()));
96
+ router.post("/api/settings/prepare", respond(req => settings.prepareChange(parseSettings(settingsChangeSchema, req.body))));
97
+ router.post("/api/settings/validate", respond(req => previewSettings(settings, tasks.definitions, parseSettings(settingsConfigSchema, req.body))));
98
+ router.post("/api/settings/config", respond(req => settings.saveConfig(parseSettings(settingsConfigSchema, req.body))));
99
+ router.post("/api/settings/credential", respond(req => settings.saveCredential(parseSettings(credentialUpdateSchema, req.body))));
100
+ router.post("/api/restart", async (req, res) => {
101
+ if (!req.body || Array.isArray(req.body) || typeof req.body !== "object" || Object.keys(req.body).length) {
102
+ res.status(400).json({ error: "Expected an empty request" });
103
+ return;
104
+ }
105
+ try {
106
+ res.json({ data: await restart() });
107
+ }
108
+ catch {
109
+ res.status(503).json({ error: "Restart could not be confirmed; inspect private logs and use CLI status before retrying" });
110
+ }
111
+ });
99
112
  router.use((_req, res) => { res.status(404).end(); });
100
113
  router.use(((error, _req, res, _next) => {
101
114
  res.status(error.status === 413 ? 413 : error.status === 415 ? 415 : 400).json({ error: "Invalid request body" });
@@ -1,9 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { errorText } from "./model.js";
4
- import { StorageError } from "./task-storage.js";
3
+ import { errorText } from "../shared/errors.js";
4
+ import { StorageError } from "../shared/errors.js";
5
5
  import { replySchema, taskSchema, taskUpdateSchema, taskIdSchema, taskListSchema, taskRunListSchema } from "./task-contract.js";
6
- import { version } from "./version.js";
6
+ import { version } from "../infrastructure/version.js";
7
7
  export function createMcpServer(service, fatal) {
8
8
  const server = new McpServer({ name: "agent-task", version });
9
9
  function register(name, description, inputSchema, execute, readOnly = true) {
@@ -25,16 +25,16 @@ export function createMcpServer(service, fatal) {
25
25
  isError: !value.ok };
26
26
  });
27
27
  }
28
- register("service_status", "Service health and active node run IDs. MCP disconnection does not cancel tasks.", z.object({}).strict(), () => service.status());
29
- register("executor_list", "Registered local ACP execution profiles and authorization settings. Approval filtering is not a sandbox.", z.object({}).strict(), () => service.listExecutors());
30
- register("task_create", "Create a task with a caller-defined DAG in nodes. Each node selects an administrator-registered ACP executorId. The coordinator advances this fixed DAG, reviews deliveries and requests missing user facts; it does not plan new nodes. Optional workspace selects an existing absolute directory; otherwise each executor uses its registered cwd. Path checks do not grant permissions; no uncertain-effect retries.", taskSchema, input => service.create(input), false);
31
- register("task_list", "List persisted task definitions.", taskListSchema, input => service.list(input.after, input.limit));
32
- register("task_get", "Get task goal, caller-defined DAG and assigned executor profiles, canonical workspace (null means executor cwd defaults), budgets and schedule.", taskIdSchema, input => service.get(input.id));
33
- register("task_update", "Pause, resume or cancel future triggers only; resume never catches up. To change a task, cancel it and create a new task. Use task_run_cancel to stop an active run; cancellation does not roll back external effects.", taskUpdateSchema, input => service.update(input), false);
34
- register("task_run_list", "List independent task runs with node progress and blocking reasons.", taskRunListSchema, input => service.runs(input.taskId, input.after, input.limit));
35
- register("task_run_get", "Get the committed plan, selected node workspaces, ACP turns, accepted deliveries, blocking questions and errors for one task run.", taskIdSchema, input => service.run(input.id));
36
- register("task_run_reply", "Answer a persisted question on the same task run. Deduplicated by requestKey; no permission escalation or uncertain-effect replay.", replySchema, input => service.reply(input), false);
37
- register("task_run_cancel", "Stop this task run; external effects are not rolled back.", taskIdSchema, input => service.cancel(input.id), false);
28
+ register("service_status", "Service health and active node run IDs. MCP disconnection does not cancel tasks.", z.object({}).strict(), () => service.supervisor.status());
29
+ register("executor_list", "Registered local ACP execution profiles and authorization settings. Approval filtering is not a sandbox.", z.object({}).strict(), () => service.executors.list());
30
+ register("task_create", "Create a task with a caller-defined DAG in nodes. Each node selects an administrator-registered ACP executorId. The coordinator advances this fixed DAG, reviews deliveries and requests missing user facts; it does not plan new nodes. Optional workspace selects an existing absolute directory; otherwise each executor uses its registered cwd. Path checks do not grant permissions; no uncertain-effect retries.", taskSchema, input => service.definitions.create(input), false);
31
+ register("task_list", "List persisted task definitions.", taskListSchema, input => service.definitions.list(input.after, input.limit));
32
+ register("task_get", "Get task goal, caller-defined DAG and assigned executor profiles, canonical workspace (null means executor cwd defaults), budgets and schedule.", taskIdSchema, input => service.definitions.get(input.id));
33
+ register("task_update", "Pause, resume or cancel future triggers only; resume never catches up. To change a task, cancel it and create a new task. Use task_run_cancel to stop an active run; cancellation does not roll back external effects.", taskUpdateSchema, input => service.definitions.update(input), false);
34
+ register("task_run_list", "List independent task runs with node progress and blocking reasons.", taskRunListSchema, input => service.runs.list(input.taskId, input.after, input.limit));
35
+ register("task_run_get", "Get the committed plan, selected node workspaces, ACP turns, accepted deliveries, blocking questions and errors for one task run.", taskIdSchema, input => service.runs.get(input.id));
36
+ register("task_run_reply", "Answer a persisted question on the same task run. Deduplicated by requestKey; no permission escalation or uncertain-effect replay.", replySchema, input => service.supervisor.reply(input), false);
37
+ register("task_run_cancel", "Stop this task run; external effects are not rolled back.", taskIdSchema, input => service.supervisor.cancel(input.id), false);
38
38
  return server;
39
39
  }
40
40
  //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=services.js.map
@@ -0,0 +1,40 @@
1
+ import { SettingsError } from "../configuration/settings.js";
2
+ import { z } from "zod";
3
+ import { configSchema } from "../configuration/contract.js";
4
+ import { executorSchema } from "../executors/validation.js";
5
+ import { identifier } from "../shared/validation.js";
6
+ export const settingsConfigSchema = z.object({
7
+ editToken: z.string().uuid(),
8
+ config: configSchema,
9
+ approveExecutors: z.array(identifier).max(100).default([]),
10
+ }).strict();
11
+ export const settingsChangeSchema = z.object({
12
+ editToken: z.string().uuid(),
13
+ change: z.discriminatedUnion("action", [
14
+ z.object({ action: z.literal("executor-add"), executor: executorSchema.omit({ unattendedApproved: true }) }).strict(),
15
+ z.object({ action: z.literal("executor-update"), id: identifier,
16
+ patch: executorSchema.omit({ unattendedApproved: true }).extend({ note: executorSchema.shape.note.removeDefault() }).partial() }).strict(),
17
+ z.object({ action: z.enum(["executor-remove", "executor-approve", "executor-revoke"]), id: identifier }).strict(),
18
+ z.object({ action: z.literal("coordinator-set"), coordinator: configSchema.shape.coordinator.unwrap() }).strict(),
19
+ z.object({ action: z.literal("coordinator-clear") }).strict(),
20
+ ]),
21
+ }).strict();
22
+ const credential = {
23
+ editToken: z.string().uuid(),
24
+ key: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(200),
25
+ };
26
+ export const credentialUpdateSchema = z.discriminatedUnion("action", [
27
+ z.object({ ...credential, action: z.literal("replace"),
28
+ value: z.string().min(1).max(16_384).refine(value => Boolean(value.trim()) && !value.includes("\0")) }).strict(),
29
+ z.object({ ...credential, action: z.literal("delete") }).strict(),
30
+ ]);
31
+ /** Reject with field paths only; rejected values and parser diagnostics can contain credentials. */
32
+ export function parseSettings(schema, value) {
33
+ const result = schema.safeParse(value);
34
+ if (!result.success) {
35
+ const fields = result.error.issues.slice(0, 8).map(issue => issue.path.join(".") || "request");
36
+ throw new SettingsError("invalid", "Invalid fields: " + fields.join(", "));
37
+ }
38
+ return result.data;
39
+ }
40
+ //# sourceMappingURL=settings-contract.js.map
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { requestTimestamp } from "../shared/validation.js";
3
+ import { nodeData, replyData, taskData, taskUpdateData } from "../tasks/validation.js";
4
+ export const nodeSchema = nodeData.extend({
5
+ prompt: z.string().trim().min(1).max(64_000),
6
+ dependsOn: nodeData.shape.dependsOn.default([]), maxRunSeconds: nodeData.shape.maxRunSeconds.default(600),
7
+ });
8
+ export const replySchema = replyData.extend({ answer: z.string().trim().min(1).max(16000) });
9
+ export const taskIdSchema = z.object({ id: z.string().uuid() }).strict();
10
+ export const taskListSchema = z.object({
11
+ after: z.number().int().nonnegative().default(0), limit: z.number().int().min(1).max(100).default(20),
12
+ }).strict();
13
+ export const taskRunListSchema = taskListSchema.extend({ taskId: z.string().uuid().optional() });
14
+ export const taskUpdateSchema = taskUpdateData;
15
+ export const taskSchema = taskData.extend({
16
+ name: z.string().trim().min(1).max(200),
17
+ prompt: z.string().trim().min(1).max(64_000).describe("Overall goal; the caller supplies the fixed DAG."),
18
+ nodes: z.array(nodeSchema).min(1).max(20).describe("Fixed DAG with an administrator-registered ACP executorId on every node."),
19
+ schedule: z.discriminatedUnion("type", [
20
+ z.object({ type: z.literal("now") }).strict(),
21
+ z.object({ type: z.literal("once"), at: requestTimestamp }).strict(),
22
+ z.object({ type: z.literal("interval"), startAt: requestTimestamp, seconds: z.number().int().min(60).max(31_536_000) }).strict(),
23
+ ]),
24
+ expiresAt: requestTimestamp.optional(),
25
+ maxRunSeconds: taskData.shape.maxRunSeconds.default(1800), maxSteps: taskData.shape.maxSteps.default(40),
26
+ maxNodeTurns: taskData.shape.maxNodeTurns.default(3), inputWaitSeconds: taskData.shape.inputWaitSeconds.default(86400),
27
+ });
28
+ //# sourceMappingURL=task-contract.js.map
@@ -0,0 +1,70 @@
1
+ import { mkdirSync, openSync, closeSync, writeFileSync, readFileSync, fstatSync, fsyncSync, realpathSync, statSync, constants } from "node:fs";
2
+ import { join, relative, isAbsolute } from "node:path";
3
+ import { OUTPUT_LIMIT } from "../shared/limits.js";
4
+ /** Handles are local to each operation and always closed, including failure paths. */
5
+ export class LocalResultFiles {
6
+ root;
7
+ constructor(root, create = false) {
8
+ this.root = root;
9
+ if (create)
10
+ mkdirSync(root, { recursive: true, mode: 0o700 });
11
+ if (realpathSync(root) !== root)
12
+ throw new Error("Result root must not be a symlink");
13
+ }
14
+ describe(path) {
15
+ const rel = relative(this.root, path);
16
+ return { canonical: realpathSync(this.root) === this.root && realpathSync(path) === path,
17
+ inside: Boolean(rel) && !rel.startsWith("..") && !isAbsolute(rel), regular: statSync(path).isFile() };
18
+ }
19
+ readable(path) {
20
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
21
+ try {
22
+ if (!fstatSync(fd).isFile())
23
+ throw new Error("Invalid result file");
24
+ }
25
+ finally {
26
+ closeSync(fd);
27
+ }
28
+ }
29
+ read(path) {
30
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
31
+ try {
32
+ const stat = fstatSync(fd);
33
+ if (!stat.isFile() || stat.size > OUTPUT_LIMIT)
34
+ throw new Error("Invalid or oversized input file");
35
+ const content = readFileSync(fd, "utf8");
36
+ if (Buffer.byteLength(content) > OUTPUT_LIMIT)
37
+ throw new Error("Input file grew beyond limit");
38
+ return content;
39
+ }
40
+ finally {
41
+ closeSync(fd);
42
+ }
43
+ }
44
+ save(taskRunId, nodeId, nodeRunId, text) {
45
+ const directory = join(this.root, taskRunId);
46
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
47
+ if (realpathSync(directory) !== directory)
48
+ throw new Error("Result directory must not be a symlink");
49
+ const path = join(directory, nodeId + "." + nodeRunId + ".txt");
50
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
51
+ try {
52
+ writeFileSync(fd, text, "utf8");
53
+ fsyncSync(fd);
54
+ }
55
+ finally {
56
+ closeSync(fd);
57
+ }
58
+ for (const dir of [directory, this.root]) {
59
+ const handle = openSync(dir, constants.O_RDONLY);
60
+ try {
61
+ fsyncSync(handle);
62
+ }
63
+ finally {
64
+ closeSync(handle);
65
+ }
66
+ }
67
+ return path;
68
+ }
69
+ }
70
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,42 @@
1
+ import { OUTPUT_LIMIT } from "../shared/limits.js";
2
+ /** Authorization and content policy; the file adapter owns OS operations and durability. */
3
+ export class Results {
4
+ files;
5
+ constructor(files) {
6
+ this.files = files;
7
+ }
8
+ get root() { return this.files.root; }
9
+ readable(path) {
10
+ const facts = this.files.describe(path);
11
+ if (!facts.canonical || !facts.inside || !facts.regular)
12
+ throw new Error("Invalid result file");
13
+ this.files.readable(path);
14
+ }
15
+ readInputFile(allowed, path, line = 1, limit) {
16
+ if (!allowed.includes(path))
17
+ throw new Error("Input file is not authorized for this run");
18
+ const facts = this.files.describe(path);
19
+ if (!facts.canonical)
20
+ throw new Error("Input file is not authorized for this run");
21
+ if (!facts.inside)
22
+ throw new Error("Input file outside result scope");
23
+ if (!facts.regular)
24
+ throw new Error("Invalid input file");
25
+ if (!Number.isInteger(line) || line < 1 || (limit !== undefined && (!Number.isInteger(limit) || limit < 1)))
26
+ throw new Error("Invalid line range");
27
+ const content = this.files.read(path);
28
+ if (line === 1 && limit === undefined)
29
+ return content;
30
+ return content.split("\n").slice(line - 1, limit === undefined ? undefined : line - 1 + limit).join("\n");
31
+ }
32
+ save(taskRunId, nodeId, nodeRunId, text) {
33
+ for (const id of [taskRunId, nodeId, nodeRunId]) {
34
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(id))
35
+ throw new Error("Invalid result identity");
36
+ }
37
+ if (Buffer.byteLength(text) > OUTPUT_LIMIT)
38
+ throw new Error("Result exceeds output limit");
39
+ return this.files.save(taskRunId, nodeId, nodeRunId, text);
40
+ }
41
+ }
42
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,13 @@
1
+ export function errorText(error) {
2
+ return (error instanceof Error ? error.message : String(error)).slice(0, 2000);
3
+ }
4
+ export class StorageError extends Error {
5
+ }
6
+ export class FileCommitError extends Error {
7
+ uncertain;
8
+ constructor(uncertain) {
9
+ super(uncertain ? "Configuration publication could not be confirmed; stop and inspect the files" : "Configuration was not published");
10
+ this.uncertain = uncertain;
11
+ }
12
+ }
13
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,3 @@
1
+ export const OUTPUT_LIMIT = 262_144;
2
+ export const EVENT_LIMIT = 2000;
3
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1,2 @@
1
+ export function iso(now = Date.now()) { return new Date(now).toISOString(); }
2
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const identifier = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/);
3
+ /** Records must already be canonical; readers never normalize or fill missing data. */
4
+ export const timestamp = z.string().datetime().refine(value => new Date(value).toISOString() === value, "Expected a canonical UTC timestamp");
5
+ export const requestTimestamp = z.string().datetime({ offset: true }).transform(value => new Date(value).toISOString());
6
+ export const absolutePath = z.string().min(1).max(4096).refine(value => value.startsWith("/") && !value.includes("\0"), "Expected an absolute path without NUL");
7
+ export const text = (max) => z.string().min(1).max(max).refine(value => value.trim().length > 0 && value.trim() === value, "Expected normalized nonempty text");
8
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1,83 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { authorizedProfile } from "../executors/registry.js";
3
+ import { iso } from "../shared/time.js";
4
+ import { validateGraph } from "./graph.js";
5
+ import { hasUncertainOutcome, initialTime, resumeSchedule } from "./schedule.js";
6
+ import { taskData, taskUpdateData } from "./validation.js";
7
+ /** Owns task definitions and future-trigger controls. Never writes a run. */
8
+ export class TaskDefinitions {
9
+ store;
10
+ executors;
11
+ workspaces;
12
+ coordinatorConfigured;
13
+ closed = false;
14
+ constructor(store, executors, workspaces, coordinatorConfigured) {
15
+ this.store = store;
16
+ this.executors = executors;
17
+ this.workspaces = workspaces;
18
+ this.coordinatorConfigured = coordinatorConfigured;
19
+ }
20
+ check() { if (this.closed)
21
+ throw new Error("Task definition service closed"); }
22
+ create(value, now = Date.now()) {
23
+ this.check();
24
+ if (!this.coordinatorConfigured)
25
+ throw new Error("Coordinator is not configured");
26
+ const input = taskData.parse(value), all = this.store.definitions();
27
+ const existing = all.find(item => item.input.requestKey === input.requestKey);
28
+ if (existing) {
29
+ if (JSON.stringify(existing.input) !== JSON.stringify(input))
30
+ throw new Error("requestKey already used with different parameters");
31
+ return existing;
32
+ }
33
+ if (all.length >= 1000)
34
+ throw new Error("Task definition limit reached");
35
+ validateGraph(input.nodes);
36
+ const executors = this.executors.snapshot(input.nodes.map(node => node.executorId));
37
+ const nextAt = initialTime(input.schedule, now);
38
+ if (input.schedule.type !== "now" && nextAt < now)
39
+ throw new Error("Schedule must start in the future");
40
+ if (input.expiresAt && Date.parse(input.expiresAt) <= Math.max(now, nextAt))
41
+ throw new Error("Deadline must follow first trigger");
42
+ const task = { id: randomUUID(), input, workspace: input.workspace === undefined ? null : this.workspaces.resolve(input.workspace),
43
+ executors, nextAt, status: "active", createdAt: iso(now) };
44
+ this.store.saveDefinition(task);
45
+ return task;
46
+ }
47
+ get(id) { this.check(); return this.store.get(id); }
48
+ list(after = 0, limit = 20) {
49
+ this.check();
50
+ const all = this.store.definitions();
51
+ return { items: all.slice(after, after + limit), next: all.length > after + limit ? after + limit : null };
52
+ }
53
+ update(value, now = Date.now()) {
54
+ this.check();
55
+ const { id, action } = taskUpdateData.parse(value);
56
+ let task = this.store.get(id);
57
+ if (action === "cancel") {
58
+ task.status = "cancelled";
59
+ task.nextAt = null;
60
+ }
61
+ else {
62
+ if (task.nextAt === null || ["cancelled", "expired"].includes(task.status))
63
+ throw new Error("Task has no future schedule");
64
+ if (action === "pause")
65
+ task.status = "paused";
66
+ else {
67
+ if (this.store.rounds(id).some(hasUncertainOutcome))
68
+ throw new Error("Task has uncertain external outcomes; future triggers require reconciliation");
69
+ task = resumeSchedule(task, now);
70
+ }
71
+ }
72
+ this.store.saveDefinition(task);
73
+ return task;
74
+ }
75
+ executorImpact(profiles) {
76
+ this.check();
77
+ return this.store.definitions().filter(task => ((task.nextAt !== null && ["active", "paused"].includes(task.status)) || this.store.activeRounds(task.id).length > 0) &&
78
+ Object.entries(task.executors).some(([id, snapshot]) => !authorizedProfile(snapshot, profiles.find(profile => profile.id === id))))
79
+ .map(task => ({ id: task.id, name: task.input.name }));
80
+ }
81
+ close() { this.closed = true; }
82
+ }
83
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1,23 @@
1
+ export function validateGraph(nodes) {
2
+ const graph = new Map(nodes.map(node => [node.id, node]));
3
+ if (!nodes.length || nodes.length > 20 || graph.size !== nodes.length)
4
+ throw new Error("Invalid or duplicate plan nodes");
5
+ const visited = new Set(), visiting = new Set();
6
+ const visit = (id) => {
7
+ if (visiting.has(id))
8
+ throw new Error("Cyclic dependency: " + id);
9
+ if (visited.has(id))
10
+ return;
11
+ const node = graph.get(id);
12
+ if (!node)
13
+ throw new Error("Unknown dependency: " + id);
14
+ if (new Set(node.dependsOn).size !== node.dependsOn.length)
15
+ throw new Error("Duplicate dependency");
16
+ visiting.add(id);
17
+ node.dependsOn.forEach(visit);
18
+ visiting.delete(id);
19
+ visited.add(id);
20
+ };
21
+ nodes.forEach(node => visit(node.id));
22
+ }
23
+ //# sourceMappingURL=graph.js.map
@@ -1,7 +1,6 @@
1
- import { randomUUID } from "node:crypto";
2
- import { iso } from "./model.js";
1
+ import { iso } from "../shared/time.js";
3
2
  /** Human facts are persisted business state, not executor permission or retry authorization. */
4
- export function askInput(run, request, seconds, now) {
3
+ export function askInput(run, request, seconds, now, questionId) {
5
4
  if (run.status !== "running")
6
5
  throw new Error("Task run is not running");
7
6
  if (run.nodes.some(node => node.status === "running"))
@@ -10,12 +9,13 @@ export function askInput(run, request, seconds, now) {
10
9
  throw new Error("Unknown question node");
11
10
  if (run.questions.length >= 20)
12
11
  throw new Error("Question limit reached");
12
+ run = structuredClone(run);
13
13
  const sessions = run.nodes.filter(node => node.status === "awaiting_review");
14
- const question = { ...request, id: randomUUID(), createdAt: iso(now), expiresAt: iso(now + seconds * 1000),
14
+ const question = { ...request, id: questionId, createdAt: iso(now), expiresAt: iso(now + seconds * 1000),
15
15
  sessionExpiresAt: sessions.length ? iso(Math.min(now + 300_000, ...sessions.map(node => Date.parse(node.startedAt) + node.definition.maxRunSeconds * 1000))) : null, reply: null };
16
16
  run.questions.push(question);
17
17
  run.status = "waiting_input";
18
- return question;
18
+ return { run, question };
19
19
  }
20
20
  export function waitingFailure(run, now) {
21
21
  if (run.status !== "waiting_input")
@@ -29,6 +29,7 @@ export function waitingFailure(run, now) {
29
29
  }
30
30
  /** Exact retries return their receipt, even after completion/cancellation; they never reactivate a run. */
31
31
  export function applyReply(run, input, now) {
32
+ run = structuredClone(run);
32
33
  const question = run.questions.find(value => value.id === input.questionId);
33
34
  if (!question)
34
35
  throw new Error("Unknown question");
@@ -36,7 +37,7 @@ export function applyReply(run, input, now) {
36
37
  if (used) {
37
38
  if (used.id !== question.id || used.reply.answer !== input.answer)
38
39
  throw new Error("Reply requestKey already used with different parameters");
39
- return false;
40
+ return undefined;
40
41
  }
41
42
  if (question.reply || run.questions.at(-1)?.id !== question.id)
42
43
  throw new Error("Question already answered or stale");
@@ -48,7 +49,7 @@ export function applyReply(run, input, now) {
48
49
  question.reply = { requestKey: input.requestKey, answer: input.answer, receivedAt: iso(now) };
49
50
  run.waitedMs += Math.max(0, now - Date.parse(question.createdAt));
50
51
  run.status = "running";
51
- return true;
52
+ return run;
52
53
  }
53
54
  export function coordinatorInput(run) {
54
55
  const question = run.questions.findLast(value => value.reply !== null);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map