@realqiyan/agent-task 0.0.1-rc.2 → 0.1.0-rc.1

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 (99) hide show
  1. package/CONTEXT.md +17 -0
  2. package/README.md +30 -7
  3. package/bin/agent-task.mjs +35 -36
  4. package/config.example.json +1 -1
  5. package/dist/{launcher.js → application/launcher.js} +49 -58
  6. package/dist/application/main.js +125 -0
  7. package/dist/application/mcp-entry.js +71 -0
  8. package/dist/{startup.js → application/startup.js} +6 -11
  9. package/dist/application/task-runtime.js +42 -0
  10. package/dist/configuration/contract.js +28 -0
  11. package/dist/{environment.js → configuration/environment.js} +1 -5
  12. package/dist/configuration/files.js +42 -0
  13. package/dist/configuration/management.js +6 -0
  14. package/dist/configuration/model.js +2 -0
  15. package/dist/configuration/policy.js +24 -0
  16. package/dist/configuration/ports.js +2 -0
  17. package/dist/{settings.js → configuration/settings.js} +54 -39
  18. package/dist/executors/environment.js +10 -0
  19. package/dist/executors/model.js +2 -0
  20. package/dist/executors/ports.js +2 -0
  21. package/dist/executors/registry.js +131 -0
  22. package/dist/executors/validation.js +9 -0
  23. package/dist/infrastructure/lock.js +55 -0
  24. package/dist/{private-files.js → infrastructure/private-files.js} +1 -7
  25. package/dist/infrastructure/sqlite.js +19 -0
  26. package/dist/infrastructure/version.js +3 -0
  27. package/dist/{acp.js → integrations/acp/connection.js} +13 -11
  28. package/dist/integrations/acp/discovery.js +271 -0
  29. package/dist/{node-execution.js → integrations/acp/execution.js} +1 -1
  30. package/dist/integrations/acp/runtime.js +43 -0
  31. package/dist/{worker.js → integrations/acp/worker.js} +7 -4
  32. package/dist/{coordinator.js → integrations/coordinator/coordinator.js} +16 -7
  33. package/dist/{coordinator-provider.js → integrations/coordinator/provider.js} +1 -1
  34. package/dist/{coordinator-storage.js → integrations/coordinator/storage.js} +2 -2
  35. package/dist/integrations/local-service/control.js +58 -0
  36. package/dist/protocol/cli/admin-client.js +45 -0
  37. package/dist/protocol/cli/commands.js +389 -0
  38. package/dist/{http-auth.js → protocol/http/auth.js} +1 -1
  39. package/dist/protocol/http/server.js +119 -0
  40. package/dist/{web.js → protocol/http/web.js} +24 -17
  41. package/dist/{mcp.js → protocol/mcp.js} +13 -13
  42. package/dist/protocol/services.js +2 -0
  43. package/dist/protocol/settings-contract.js +37 -0
  44. package/dist/protocol/stdio.js +71 -0
  45. package/dist/protocol/task-contract.js +28 -0
  46. package/dist/results/files.js +70 -0
  47. package/dist/results/ports.js +2 -0
  48. package/dist/results/service.js +42 -0
  49. package/dist/shared/errors.js +13 -0
  50. package/dist/shared/limits.js +3 -0
  51. package/dist/shared/time.js +2 -0
  52. package/dist/shared/validation.js +8 -0
  53. package/dist/tasks/definitions.js +86 -0
  54. package/dist/tasks/graph.js +23 -0
  55. package/dist/{human-input.js → tasks/human-input.js} +8 -7
  56. package/dist/tasks/model.js +2 -0
  57. package/dist/tasks/ports.js +2 -0
  58. package/dist/tasks/runs.js +263 -0
  59. package/dist/tasks/schedule.js +36 -0
  60. package/dist/tasks/sessions.js +62 -0
  61. package/dist/tasks/storage/records.js +45 -0
  62. package/dist/{task-storage.js → tasks/storage/store.js} +28 -40
  63. package/dist/tasks/supervisor.js +227 -0
  64. package/dist/tasks/validation.js +24 -0
  65. package/dist/web/settings-view.js +46 -33
  66. package/docs/adr/0001-task-request-scope.md +3 -0
  67. package/docs/adr/0002-mcp-workspace-source.md +3 -0
  68. package/docs/adr/0003-adapter-installation.md +3 -0
  69. package/docs/architecture-guidelines.md +8 -6
  70. package/docs/cli.md +57 -0
  71. package/docs/deployment.md +46 -70
  72. package/docs/design-proposal.md +5 -2
  73. package/docs/release.md +14 -15
  74. package/docs/technical-design.md +49 -26
  75. package/docs/validation.md +92 -18
  76. package/docs/web-console.md +16 -11
  77. package/package.json +9 -7
  78. package/scripts/install-lib.mjs +7 -116
  79. package/scripts/mcp-call.mjs +1 -1
  80. package/scripts/probe-agent.mjs +14 -12
  81. package/scripts/service-runner.mjs +9 -4
  82. package/scripts/setup-wizard.mjs +38 -16
  83. package/scripts/setup.mjs +69 -108
  84. package/deploy/agent-task.service.example +0 -17
  85. package/dist/admin.js +0 -31
  86. package/dist/config.js +0 -72
  87. package/dist/executor-runtime.js +0 -37
  88. package/dist/lock.js +0 -43
  89. package/dist/main.js +0 -175
  90. package/dist/model.js +0 -25
  91. package/dist/results.js +0 -79
  92. package/dist/settings-contract.js +0 -16
  93. package/dist/task-contract.js +0 -37
  94. package/dist/task-model.js +0 -2
  95. package/dist/tasks.js +0 -508
  96. package/dist/version.js +0 -3
  97. package/scripts/uninstall.mjs +0 -57
  98. /package/dist/{permissions.js → executors/permissions.js} +0 -0
  99. /package/dist/{workspace.js → infrastructure/workspace.js} +0 -0
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { executorTypeSchema } from "../executors/validation.js";
3
+ import { baseEnvKeys } from "./policy.js";
4
+ const envKey = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
5
+ const modelId = z.string().min(1).max(200);
6
+ const coordinatorSchema = z.discriminatedUnion("provider", [
7
+ z.object({ provider: z.enum(["openai", "anthropic", "deepseek"]), model: modelId }).strict(),
8
+ z.object({
9
+ provider: z.literal("openai-compatible"), model: modelId,
10
+ api: z.enum(["openai-completions", "openai-responses"]).default("openai-completions"),
11
+ baseUrl: z.string().url().refine(value => {
12
+ const url = new URL(value);
13
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
14
+ }, "baseUrl must be HTTP(S) without credentials, query or fragment"),
15
+ apiKeyEnv: envKey.refine(key => !baseEnvKeys.includes(key), "Credential must not use a base environment key"),
16
+ contextWindow: z.number().int().positive(),
17
+ maxTokens: z.number().int().positive(),
18
+ reasoning: z.boolean().default(false),
19
+ }).strict(),
20
+ ]).refine(value => value.provider !== "openai-compatible" || value.maxTokens <= value.contextWindow, "maxTokens must not exceed contextWindow");
21
+ export const configSchema = z.object({
22
+ dataDir: z.string().min(1),
23
+ port: z.number().int().min(0).max(65535).default(8765),
24
+ tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).default("AGENT_TASK_TOKEN"),
25
+ coordinator: coordinatorSchema.optional(),
26
+ disabledExecutors: z.array(executorTypeSchema).max(4).refine(types => new Set(types).size === types.length, "Disabled executor types must be unique").default([]),
27
+ }).strict();
28
+ //# sourceMappingURL=contract.js.map
@@ -1,5 +1,4 @@
1
- import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "./config.js";
2
- import { readPrivateText } from "./private-files.js";
1
+ import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "./policy.js";
3
2
  export function parseEnvironment(text) {
4
3
  let value;
5
4
  try {
@@ -13,9 +12,6 @@ export function parseEnvironment(text) {
13
12
  throw new Error("Invalid environment file");
14
13
  return value;
15
14
  }
16
- export function readEnvironment(path) {
17
- return parseEnvironment(readPrivateText(path));
18
- }
19
15
  /** Credential names and values must stay separate, including historical token aliases. */
20
16
  export function validateServiceEnvironment(config, environment) {
21
17
  const token = environment[config.tokenEnv], adminToken = environment[ADMIN_TOKEN_ENV];
@@ -0,0 +1,42 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { checkDataDirectory, readPrivateText, replacePrivateText } from "../infrastructure/private-files.js";
4
+ import { configSchema } from "./contract.js";
5
+ import { parseEnvironment } from "./environment.js";
6
+ import { validateConfig } from "./policy.js";
7
+ export const configurationPaths = { checkDataDirectory };
8
+ export function loadConfig(path) {
9
+ let value;
10
+ try {
11
+ value = JSON.parse(readPrivateText(path));
12
+ }
13
+ catch {
14
+ throw new Error("Cannot read configuration JSON");
15
+ }
16
+ return validateConfig(configSchema.parse(value));
17
+ }
18
+ export function readEnvironment(path) { return parseEnvironment(readPrivateText(path)); }
19
+ export class LocalSettingsFiles {
20
+ configPath;
21
+ environmentPath;
22
+ constructor(configPath, environmentPath) {
23
+ this.configPath = configPath;
24
+ this.environmentPath = environmentPath;
25
+ if (configPath === environmentPath)
26
+ throw new Error("Configuration and environment must be separate files");
27
+ }
28
+ read() {
29
+ const configText = readPrivateText(this.configPath), environmentText = readPrivateText(this.environmentPath);
30
+ return { configText, environmentText, config: configSchema.parse(JSON.parse(configText)), environment: parseEnvironment(environmentText) };
31
+ }
32
+ publish(target, value) {
33
+ replacePrivateText(target === "config" ? this.configPath : this.environmentPath, JSON.stringify(value, null, 2) + "\n");
34
+ }
35
+ }
36
+ /** A stable launch locator is the only runtime input outside the selected dataDir. */
37
+ export function startupPaths(home) {
38
+ const { dataDir } = z.object({ dataDir: z.string() }).strict().parse(JSON.parse(readPrivateText(join(home, "launch.json"))));
39
+ checkDataDirectory(dataDir);
40
+ return { dataDir, configPath: join(dataDir, "config.json"), environmentPath: join(dataDir, "environment.json") };
41
+ }
42
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,6 @@
1
+ /** Configuration preview and task impact are one management operation for all clients. */
2
+ export function previewSettings(settings, tasks, input) {
3
+ const preview = settings.preview(input);
4
+ return { ...preview, affectedTasks: tasks.executorImpact(preview.config.disabledExecutors) };
5
+ }
6
+ //# sourceMappingURL=management.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,24 @@
1
+ export const ADMIN_TOKEN_ENV = "AGENT_TASK_ADMIN_TOKEN";
2
+ export const baseEnvKeys = ["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "NODE_OPTIONS", "NODE_PATH", "__proto__", "constructor", "prototype"];
3
+ export function coordinatorCredentialEnv(config) {
4
+ switch (config.provider) {
5
+ case "openai": return "OPENAI_API_KEY";
6
+ case "anthropic": return "ANTHROPIC_API_KEY";
7
+ case "deepseek": return "DEEPSEEK_API_KEY";
8
+ case "openai-compatible": return config.apiKeyEnv;
9
+ }
10
+ }
11
+ export function validateConfig(value) {
12
+ const config = structuredClone(value);
13
+ if (!config.dataDir.startsWith("/") || /[\x00-\x1f\x7f]/.test(config.dataDir))
14
+ throw new Error("dataDir must be absolute without control characters");
15
+ const coordinatorKey = config.coordinator ? coordinatorCredentialEnv(config.coordinator) : undefined;
16
+ if (/^(NODE_|LD_|DYLD_)/.test(config.tokenEnv) || [...baseEnvKeys, ADMIN_TOKEN_ENV, "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY", coordinatorKey].includes(config.tokenEnv))
17
+ throw new Error("tokenEnv must be separate from base environment, administrator and model credentials");
18
+ if (coordinatorKey === ADMIN_TOKEN_ENV)
19
+ throw new Error("Administrator token must not be used as a model credential");
20
+ if (new Set(config.disabledExecutors).size !== config.disabledExecutors.length)
21
+ throw new Error("Duplicate disabled executor type");
22
+ return config;
23
+ }
24
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -1,9 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { z } from "zod";
3
- import { ADMIN_TOKEN_ENV, configSchema, coordinatorCredentialEnv, validateConfig } from "./config.js";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv, validateConfig } from "./policy.js";
4
4
  import { parseEnvironment, validateServiceEnvironment } from "./environment.js";
5
- import { checkDataDirectory, FileCommitError, readPrivateText, replacePrivateText } from "./private-files.js";
6
- import { credentialUpdateSchema, settingsConfigSchema } from "./settings-contract.js";
5
+ import { FileCommitError } from "../shared/errors.js";
7
6
  export class SettingsError extends Error {
8
7
  code;
9
8
  constructor(code, message) {
@@ -14,15 +13,6 @@ export class SettingsError extends Error {
14
13
  const protectedKeys = new Set(["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "NODE_OPTIONS", "NODE_PATH",
15
14
  "PI_ACP_PI_COMMAND", "AGENT_TASK_TOKEN", ADMIN_TOKEN_ENV, "__proto__", "constructor", "prototype"]);
16
15
  const modelKeys = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY"];
17
- function parse(schema, value) {
18
- const result = schema.safeParse(value);
19
- if (!result.success) {
20
- // Field paths only: parser diagnostics and rejected values may contain credentials.
21
- const fields = result.error.issues.slice(0, 8).map(issue => issue.path.join(".") || "request");
22
- throw new SettingsError("invalid", "Invalid fields: " + fields.join(", "));
23
- }
24
- return result.data;
25
- }
26
16
  function referenced(config) {
27
17
  return new Set(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : []);
28
18
  }
@@ -30,10 +20,15 @@ function editableKey(key, config) {
30
20
  return !protectedKeys.has(key) && key !== config.tokenEnv && !/^(NODE_|LD_|DYLD_)/.test(key) &&
31
21
  (/(?:_KEY|_TOKEN|_SECRET|_PASSWORD)$/.test(key) || referenced(config).has(key));
32
22
  }
23
+ export function configChanges(before, after) {
24
+ return Object.keys(after).concat(Object.keys(before))
25
+ .filter((key, index, keys) => keys.indexOf(key) === index && !isDeepStrictEqual(before[key], after[key]))
26
+ .map(field => ({ field, before: before[field] ?? null, after: after[field] ?? null }));
27
+ }
33
28
  /** Disk settings are next-start inputs only. This service never mutates live objects or process.env. */
34
29
  export class Settings {
35
- configPath;
36
- environmentPath;
30
+ files;
31
+ paths;
37
32
  edits = new Map();
38
33
  initial;
39
34
  running;
@@ -42,11 +37,9 @@ export class Settings {
42
37
  closed = false;
43
38
  uncertain = false;
44
39
  startedAt = new Date().toISOString();
45
- constructor(configPath, environmentPath, running, base, environment) {
46
- this.configPath = configPath;
47
- this.environmentPath = environmentPath;
48
- if (configPath === environmentPath)
49
- throw new SettingsError("invalid", "Configuration and environment must be separate files");
40
+ constructor(files, paths, running, base, environment) {
41
+ this.files = files;
42
+ this.paths = paths;
50
43
  this.running = structuredClone(running);
51
44
  this.base = { ...base };
52
45
  this.runningEnvironment = { ...environment };
@@ -56,8 +49,7 @@ export class Settings {
56
49
  if (this.closed)
57
50
  throw new SettingsError("unavailable", "Configuration management is closed");
58
51
  try {
59
- const configText = readPrivateText(this.configPath), environmentText = readPrivateText(this.environmentPath);
60
- return { configText, environmentText, config: configSchema.parse(JSON.parse(configText)), environment: parseEnvironment(environmentText) };
52
+ return this.files.read();
61
53
  }
62
54
  catch {
63
55
  throw new SettingsError("unavailable", "Cannot read private configuration files; inspect their format, ownership and permissions offline");
@@ -96,18 +88,16 @@ export class Settings {
96
88
  }
97
89
  return snapshot;
98
90
  }
99
- candidate(value) {
100
- const input = parse(settingsConfigSchema, value), snapshot = this.editing(input.editToken);
91
+ candidate(input) {
92
+ const snapshot = this.editing(input.editToken);
101
93
  const environment = { ...this.base, ...snapshot.environment };
102
94
  let config;
103
95
  try {
104
96
  config = validateConfig(input.config);
105
- checkDataDirectory(config.dataDir);
97
+ this.paths.checkDataDirectory(config.dataDir);
106
98
  }
107
- catch (error) {
108
- if (error instanceof z.ZodError)
109
- throw new SettingsError("invalid", "Invalid configuration fields");
110
- throw new SettingsError("invalid", "Local validation failed: check data directory, workspaces and credential isolation");
99
+ catch {
100
+ throw new SettingsError("invalid", "Local validation failed: check data directory, executor selection and credential isolation");
111
101
  }
112
102
  const previous = snapshot.config.coordinator, next = config.coordinator;
113
103
  const endpoint = (model) => model.provider === "openai-compatible" ? model.baseUrl : model.provider;
@@ -131,19 +121,44 @@ export class Settings {
131
121
  return config;
132
122
  }
133
123
  validate(value) { return this.candidate(value); }
134
- saveConfig(value) {
135
- const config = this.candidate(value);
136
- const snapshot = this.editing(parse(settingsConfigSchema, value).editToken);
124
+ preview(input) {
125
+ const config = this.candidate(input);
126
+ return { config, changes: configChanges(this.editing(input.editToken).config, config) };
127
+ }
128
+ diff() {
129
+ const saved = this.snapshot().config;
130
+ return { changes: configChanges(this.running, saved), ...this.status() };
131
+ }
132
+ /** Convenience commands create a draft; all publication still uses saveConfig. */
133
+ prepareChange(value) {
134
+ const { editToken, change } = value;
135
+ const config = structuredClone(this.editing(editToken).config);
136
+ if (change.action === "coordinator-set")
137
+ config.coordinator = change.coordinator;
138
+ else if (change.action === "coordinator-clear")
139
+ delete config.coordinator;
140
+ else if (change.action === "executor-enable") {
141
+ config.disabledExecutors = config.disabledExecutors.filter(type => type !== change.id);
142
+ }
143
+ else if (!config.disabledExecutors.includes(change.id)) {
144
+ config.disabledExecutors.push(change.id);
145
+ }
146
+ const input = { editToken, config };
147
+ return { ...input, config: this.candidate(input) };
148
+ }
149
+ saveConfig(input) {
150
+ const config = this.candidate(input);
151
+ const snapshot = this.editing(input.editToken);
137
152
  if (config.tokenEnv !== snapshot.config.tokenEnv) {
138
153
  snapshot.environment[config.tokenEnv] = snapshot.environment[snapshot.config.tokenEnv];
139
154
  // Publish an alias first. Either configuration still uses the same service token.
140
- this.publish(this.environmentPath, snapshot.environment);
155
+ this.publish("environment", snapshot.environment);
141
156
  }
142
- this.publish(this.configPath, config);
157
+ this.publish("config", config);
143
158
  return this.get();
144
159
  }
145
- saveCredential(value) {
146
- const input = parse(credentialUpdateSchema, value), snapshot = this.editing(input.editToken);
160
+ saveCredential(input) {
161
+ const snapshot = this.editing(input.editToken);
147
162
  if (!editableKey(input.key, snapshot.config) || !editableKey(input.key, this.running) ||
148
163
  [snapshot.environment[snapshot.config.tokenEnv], snapshot.environment[ADMIN_TOKEN_ENV]].includes(snapshot.environment[input.key]))
149
164
  throw new SettingsError("invalid", "This environment key is not console-managed; new credential names must end in _KEY, _TOKEN, _SECRET or _PASSWORD");
@@ -161,12 +176,12 @@ export class Settings {
161
176
  }
162
177
  // Recheck the complete environment format, not only the edited value.
163
178
  parseEnvironment(JSON.stringify(snapshot.environment));
164
- this.publish(this.environmentPath, snapshot.environment);
179
+ this.publish("environment", snapshot.environment);
165
180
  return this.get();
166
181
  }
167
- publish(path, value) {
182
+ publish(target, value) {
168
183
  try {
169
- replacePrivateText(path, JSON.stringify(value, null, 2) + "\n");
184
+ this.files.publish(target, value);
170
185
  }
171
186
  catch (error) {
172
187
  if (error instanceof FileCommitError && error.uncertain)
@@ -0,0 +1,10 @@
1
+ import { ADMIN_TOKEN_ENV, coordinatorCredentialEnv } from "../configuration/policy.js";
2
+ /** Private service inputs never enter workers, even through an alias with the same value. */
3
+ export function executorEnvironment(native, privateValues, config) {
4
+ const blocked = new Set([...Object.keys(privateValues), config.tokenEnv, ADMIN_TOKEN_ENV,
5
+ ...(config.coordinator ? [coordinatorCredentialEnv(config.coordinator)] : [])]);
6
+ const secrets = new Set([...Object.values(privateValues), ...[...blocked].map(key => native[key])].filter(Boolean));
7
+ return Object.fromEntries(Object.entries(native).filter(([key, value]) => value !== undefined && !blocked.has(key) && !secrets.has(value) &&
8
+ !/^(AGENT_TASK_|NODE_|LD_|DYLD_)/.test(key) && key !== "PI_ACP_PI_COMMAND"));
9
+ }
10
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,131 @@
1
+ import { errorText } from "../shared/errors.js";
2
+ export function sameExecutor(a, b) {
3
+ return a.id === b.id && a.type === b.type && a.nativePath === b.nativePath;
4
+ }
5
+ export function authorizedProfile(snapshot, current) {
6
+ return Boolean(snapshot && current && sameExecutor(snapshot, current));
7
+ }
8
+ function status(executor, state = "ready") {
9
+ return { id: executor.id, type: executor.type, nativePath: executor.nativePath,
10
+ state, adapterVersion: executor.adapterVersion, error: null };
11
+ }
12
+ /** Owns the ready executor set and adapter installation lifetime, not native tool permissions. */
13
+ export class ExecutorRegistry {
14
+ discovery;
15
+ profiles;
16
+ states;
17
+ disabled;
18
+ installing = new Map();
19
+ closed = false;
20
+ closing;
21
+ constructor(profiles, discovery, disabled = []) {
22
+ this.discovery = discovery;
23
+ this.disabled = new Set(disabled);
24
+ const found = discovery?.discover();
25
+ this.profiles = structuredClone((found?.executors ?? profiles).filter(profile => !this.disabled.has(profile.type)));
26
+ this.states = found ? found.states.map(entry => this.disabled.has(entry.type)
27
+ ? { ...entry, state: "disabled", error: null } : { ...entry })
28
+ : profiles.map(profile => status(profile, this.disabled.has(profile.type) ? "disabled" : "ready"));
29
+ }
30
+ checkOpen() { if (this.closed)
31
+ throw new Error("Executor registry closed"); }
32
+ list() { this.checkOpen(); return { executors: structuredClone(this.states) }; }
33
+ refresh() {
34
+ this.checkOpen();
35
+ if (!this.discovery)
36
+ return this.list();
37
+ const found = this.discovery.discover();
38
+ this.profiles = structuredClone(found.executors.filter(profile => !this.disabled.has(profile.type) && !this.installing.has(profile.type)));
39
+ this.states = found.states.map(entry => {
40
+ if (this.disabled.has(entry.type))
41
+ return { ...entry, state: "disabled", error: null };
42
+ if (this.installing.has(entry.type))
43
+ return { ...entry, state: "installing", error: null };
44
+ return { ...entry };
45
+ });
46
+ for (const entry of this.states) {
47
+ if (entry.state === "missing" && entry.nativePath !== null)
48
+ this.install(entry, false);
49
+ }
50
+ return this.list();
51
+ }
52
+ upgrade(id) {
53
+ this.checkOpen();
54
+ const entry = this.states.find(item => item.id === id);
55
+ if (!this.discovery || !entry || !entry.nativePath)
56
+ throw new Error("Executor is not installed: " + id);
57
+ if (this.disabled.has(entry.type))
58
+ throw new Error("Executor is disabled: " + id);
59
+ if (entry.type === "opencode")
60
+ throw new Error("OpenCode uses its native ACP command; update the native CLI directly");
61
+ this.install(entry, true);
62
+ return this.list();
63
+ }
64
+ install(entry, upgrade) {
65
+ const discovery = this.discovery;
66
+ if (!discovery || this.installing.has(entry.type))
67
+ return;
68
+ const controller = new AbortController(), expectedPath = entry.nativePath;
69
+ this.profiles = this.profiles.filter(profile => profile.type !== entry.type);
70
+ Object.assign(entry, { state: "installing", error: null });
71
+ const done = Promise.resolve().then(async () => {
72
+ try {
73
+ if (this.closed || controller.signal.aborted)
74
+ return;
75
+ const executor = await discovery.install(entry.type, upgrade, controller.signal);
76
+ if (this.closed)
77
+ return;
78
+ if (executor.type !== entry.type || executor.id !== entry.id || executor.nativePath !== expectedPath) {
79
+ throw new Error("Native executor changed during adapter installation; refresh discovery");
80
+ }
81
+ discovery.check(executor);
82
+ this.profiles = [...this.profiles.filter(profile => profile.type !== entry.type), structuredClone(executor)];
83
+ this.states = this.states.map(item => item.type === entry.type ? status(executor) : item);
84
+ }
85
+ catch (error) {
86
+ if (!this.closed)
87
+ this.states = this.states.map(item => item.type === entry.type ?
88
+ { ...item, state: "error", error: errorText(error) } : item);
89
+ }
90
+ finally {
91
+ this.installing.delete(entry.type);
92
+ }
93
+ });
94
+ this.installing.set(entry.type, { controller, done });
95
+ }
96
+ ready(id) {
97
+ const profile = this.profiles.find(item => item.id === id);
98
+ if (!profile) {
99
+ const entry = this.states.find(item => item.id === id);
100
+ throw new Error("Executor is not ready: " + id + (entry ? " (" + entry.state + ")" : ""));
101
+ }
102
+ this.discovery?.check(profile);
103
+ return profile;
104
+ }
105
+ snapshot(ids) {
106
+ this.checkOpen();
107
+ const result = Object.create(null);
108
+ for (const id of ids)
109
+ result[id] = structuredClone(this.ready(id));
110
+ return result;
111
+ }
112
+ authorize(snapshot) {
113
+ this.checkOpen();
114
+ if (!snapshot)
115
+ throw new Error("Executor snapshot is missing");
116
+ const current = this.ready(snapshot.id);
117
+ if (!authorizedProfile(snapshot, current))
118
+ throw new Error("Native executor changed; existing task assignment cannot be replaced");
119
+ return structuredClone(current);
120
+ }
121
+ close() {
122
+ if (this.closing)
123
+ return this.closing;
124
+ this.closed = true;
125
+ for (const entry of this.installing.values())
126
+ entry.controller.abort();
127
+ this.closing = Promise.all([...this.installing.values()].map(entry => entry.done)).then(() => undefined);
128
+ return this.closing;
129
+ }
130
+ }
131
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ import { absolutePath, identifier } from "../shared/validation.js";
3
+ export const executorTypeSchema = z.enum(["pi", "codex", "claude", "opencode"]);
4
+ export const executorRecord = z.object({
5
+ id: identifier, type: executorTypeSchema, nativePath: absolutePath,
6
+ command: absolutePath, args: z.array(z.string().max(4000)).max(100),
7
+ adapterVersion: z.string().min(1).max(200).nullable(),
8
+ }).strict();
9
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1,55 @@
1
+ import { DatabaseSync } from "./sqlite.js";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ /** The stable installation lock prevents two launchers during a directory switch. */
6
+ export async function acquireLauncherLock(home) {
7
+ const deadline = Date.now() + 1500;
8
+ for (;;) {
9
+ const db = new DatabaseSync(join(home, "launcher-lock.sqlite"));
10
+ try {
11
+ db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
12
+ }
13
+ catch (error) {
14
+ db.close();
15
+ const code = error.errcode;
16
+ if (code !== 5 && code !== 6)
17
+ throw new Error("Cannot acquire launcher ownership; inspect the installation filesystem", { cause: error });
18
+ if (Date.now() >= deadline)
19
+ throw new Error("Another launcher owns this installation", { cause: error });
20
+ // On a new SQLite file, concurrent readers can all lose the first exclusive-lock attempt.
21
+ // Release each failed connection and stagger retries before any service work starts.
22
+ await delay(25 + Math.floor(Math.random() * 25));
23
+ continue;
24
+ }
25
+ let released = false;
26
+ return () => { if (!released) {
27
+ db.close();
28
+ released = true;
29
+ } };
30
+ }
31
+ }
32
+ /** A dedicated SQLite transaction owns an OS lock until close or process death.
33
+ * Local filesystems only. Never unlink this file: that would create a second lock identity.
34
+ */
35
+ export async function acquireLock(directory) {
36
+ if (!["linux", "darwin"].includes(process.platform))
37
+ throw new Error("Supported platforms: Linux and macOS");
38
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
39
+ const db = new DatabaseSync(join(directory, "service-lock.sqlite"));
40
+ try {
41
+ db.exec("PRAGMA busy_timeout=0; PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE");
42
+ }
43
+ catch (error) {
44
+ db.close();
45
+ throw new Error("State directory is locked or its filesystem does not support SQLite locking", { cause: error });
46
+ }
47
+ let released = false;
48
+ return async () => {
49
+ if (released)
50
+ return;
51
+ db.close();
52
+ released = true;
53
+ };
54
+ }
55
+ //# sourceMappingURL=lock.js.map
@@ -1,3 +1,4 @@
1
+ import { FileCommitError } from "../shared/errors.js";
1
2
  import { existsSync, constants, openSync, closeSync, fstatSync, lstatSync, realpathSync, readFileSync, writeFileSync, fsyncSync, renameSync, unlinkSync } from "node:fs";
2
3
  import { dirname, basename, join, isAbsolute, resolve } from "node:path";
3
4
  import { randomUUID } from "node:crypto";
@@ -44,13 +45,6 @@ export function readPrivateText(path) {
44
45
  closeSync(fd);
45
46
  }
46
47
  }
47
- export class FileCommitError extends Error {
48
- uncertain;
49
- constructor(uncertain) {
50
- super(uncertain ? "Configuration publication could not be confirmed; stop and inspect the files" : "Configuration was not published");
51
- this.uncertain = uncertain;
52
- }
53
- }
54
48
  /** Caller owns the configuration. No await between comparison, validation and publication. */
55
49
  export function replacePrivateText(path, text) {
56
50
  const temporary = join(dirname(path), "." + basename(path) + "." + randomUUID() + ".tmp");
@@ -0,0 +1,19 @@
1
+ import { createRequire } from "node:module";
2
+ // Node 22 warns on loading SQLite. Suppress only that known notice during the
3
+ // synchronous import, then restore normal warning handling for all other code.
4
+ const emitWarning = process.emitWarning;
5
+ let sqlite;
6
+ try {
7
+ process.emitWarning = (warning, ...args) => {
8
+ if (warning === "SQLite is an experimental feature and might change at any time"
9
+ && args[0] === "ExperimentalWarning")
10
+ return;
11
+ Reflect.apply(emitWarning, process, [warning, ...args]);
12
+ };
13
+ sqlite = createRequire(import.meta.url)("node:sqlite");
14
+ }
15
+ finally {
16
+ process.emitWarning = emitWarning;
17
+ }
18
+ export const DatabaseSync = sqlite.DatabaseSync;
19
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1,3 @@
1
+ import { readFileSync } from "node:fs";
2
+ export const version = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
3
+ //# sourceMappingURL=version.js.map
@@ -2,10 +2,11 @@ import { spawn } from "node:child_process";
2
2
  import { Readable, Writable } from "node:stream";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import * as acp from "@agentclientprotocol/sdk";
5
- import { executorCommand } from "./executor-runtime.js";
6
- import { readInputFile } from "./results.js";
7
- import { permissionDecision } from "./permissions.js";
8
- import { version } from "./version.js";
5
+ import { executorLaunch } from "./runtime.js";
6
+ import { Results } from "../../results/service.js";
7
+ import { LocalResultFiles } from "../../results/files.js";
8
+ import { permissionDecision } from "../../executors/permissions.js";
9
+ import { version } from "../../infrastructure/version.js";
9
10
  /** Project only public progress. Queue every piece before yielding so concurrent notifications cannot reorder output. */
10
11
  export function progressEvents(update) {
11
12
  if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
@@ -38,7 +39,7 @@ function signalGroup(child, signal) {
38
39
  }
39
40
  }
40
41
  }
41
- /** Kill the whole group even if its leader already exited; Linux systemd additionally owns a cgroup; macOS does not. */
42
+ /** Kill the whole group even if its leader already exited. */
42
43
  export async function terminateGroup(child) {
43
44
  signalGroup(child, "SIGTERM");
44
45
  await delay(300);
@@ -47,11 +48,12 @@ export async function terminateGroup(child) {
47
48
  child.stdout?.destroy();
48
49
  child.stderr?.destroy();
49
50
  }
50
- export async function openAcp(executor, signal, emit, resultsRoot, inputFiles = []) {
51
+ export async function openAcp(executor, workspace, signal, emit, resultsRoot, inputFiles = []) {
51
52
  signal.throwIfAborted();
52
- const launch = executorCommand(executor.type, process.env);
53
+ const results = new Results(new LocalResultFiles(resultsRoot));
54
+ const launch = executorLaunch(executor, process.env);
53
55
  const child = spawn(launch.command, launch.args, {
54
- cwd: executor.cwd, env: process.env, detached: true, stdio: ["pipe", "pipe", "pipe"],
56
+ cwd: workspace, env: launch.env, detached: true, stdio: ["pipe", "pipe", "pipe"],
55
57
  });
56
58
  const controller = new AbortController();
57
59
  const cancel = AbortSignal.any([signal, controller.signal]);
@@ -103,7 +105,7 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
103
105
  cancel.throwIfAborted();
104
106
  if (ctx.params.sessionId !== sessionId)
105
107
  throw new Error("Wrong input-file session");
106
- return { content: readInputFile(resultsRoot, inputFiles, ctx.params.path, ctx.params.line ?? 1, ctx.params.limit ?? undefined) };
108
+ return { content: results.readInputFile(inputFiles, ctx.params.path, ctx.params.line ?? 1, ctx.params.limit ?? undefined) };
107
109
  }
108
110
  catch (error) {
109
111
  controller.abort(error);
@@ -134,7 +136,7 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
134
136
  if (init.protocolVersion !== acp.PROTOCOL_VERSION)
135
137
  throw new Error("Incompatible ACP protocol version");
136
138
  await report("agent", { name: init.agentInfo?.name ?? null, version: init.agentInfo?.version ?? null });
137
- const session = await connection.agent.request(acp.methods.agent.session.new, { cwd: executor.cwd, mcpServers: [] });
139
+ const session = await connection.agent.request(acp.methods.agent.session.new, { cwd: workspace, mcpServers: [] });
138
140
  sessionId = session.sessionId;
139
141
  await report("session", sessionId);
140
142
  cancel.throwIfAborted();
@@ -166,4 +168,4 @@ export async function openAcp(executor, signal, emit, resultsRoot, inputFiles =
166
168
  throw error;
167
169
  }
168
170
  }
169
- //# sourceMappingURL=acp.js.map
171
+ //# sourceMappingURL=connection.js.map