@actagent/acpx 2026.6.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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Builds isolated Codex config for ACPX sessions. It preserves safe inherited
3
+ * runtime options while rendering only trusted project entries for the session.
4
+ */
5
+ import path from "node:path";
6
+
7
+ function stripTomlComment(line: string): string {
8
+ let quote: "'" | '"' | null = null;
9
+ let escaping = false;
10
+ for (let index = 0; index < line.length; index += 1) {
11
+ const ch = line[index];
12
+ if (escaping) {
13
+ escaping = false;
14
+ continue;
15
+ }
16
+ if (quote === '"' && ch === "\\") {
17
+ escaping = true;
18
+ continue;
19
+ }
20
+ if (quote) {
21
+ if (ch === quote) {
22
+ quote = null;
23
+ }
24
+ continue;
25
+ }
26
+ if (ch === "'" || ch === '"') {
27
+ quote = ch;
28
+ continue;
29
+ }
30
+ if (ch === "#") {
31
+ return line.slice(0, index);
32
+ }
33
+ }
34
+ return line;
35
+ }
36
+
37
+ function parseTomlString(value: string): string | undefined {
38
+ const trimmed = value.trim();
39
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
40
+ try {
41
+ return JSON.parse(trimmed) as string;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
47
+ return trimmed.slice(1, -1);
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ function parseTomlDottedKey(value: string): string[] {
53
+ const parts: string[] = [];
54
+ let current = "";
55
+ let quote: "'" | '"' | null = null;
56
+ let escaping = false;
57
+
58
+ for (const ch of value.trim()) {
59
+ if (escaping) {
60
+ current += ch;
61
+ escaping = false;
62
+ continue;
63
+ }
64
+ if (quote === '"' && ch === "\\") {
65
+ current += ch;
66
+ escaping = true;
67
+ continue;
68
+ }
69
+ if (quote) {
70
+ current += ch;
71
+ if (ch === quote) {
72
+ quote = null;
73
+ }
74
+ continue;
75
+ }
76
+ if (ch === "'" || ch === '"') {
77
+ quote = ch;
78
+ current += ch;
79
+ continue;
80
+ }
81
+ if (ch === ".") {
82
+ parts.push(current.trim());
83
+ current = "";
84
+ continue;
85
+ }
86
+ current += ch;
87
+ }
88
+ if (current.trim()) {
89
+ parts.push(current.trim());
90
+ }
91
+ return parts.map((part) => parseTomlString(part) ?? part);
92
+ }
93
+
94
+ function parseProjectHeader(line: string): string | undefined {
95
+ const trimmed = line.trim();
96
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) {
97
+ return undefined;
98
+ }
99
+ const parts = parseTomlDottedKey(trimmed.slice(1, -1));
100
+ return parts.length === 2 && parts[0] === "projects" ? parts[1] : undefined;
101
+ }
102
+
103
+ function parseTrustedInlineProjectEntries(value: string): string[] {
104
+ const trusted: string[] = [];
105
+ const entryPattern =
106
+ /(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*\{(?<body>[^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
107
+ for (const match of value.matchAll(entryPattern)) {
108
+ const key = match.groups?.key;
109
+ const body = match.groups?.body;
110
+ if (!key || !body || !/\btrust_level\s*=\s*["']trusted["']/.test(body)) {
111
+ continue;
112
+ }
113
+ const projectPath = parseTomlString(key) ?? key.trim();
114
+ if (projectPath) {
115
+ trusted.push(projectPath);
116
+ }
117
+ }
118
+ return trusted;
119
+ }
120
+
121
+ /** Extract trusted project paths from Codex TOML config. */
122
+ export function extractTrustedCodexProjectPaths(configToml: string): string[] {
123
+ const trusted = new Set<string>();
124
+ let currentProjectPath: string | undefined;
125
+ let inProjectsTable = false;
126
+
127
+ for (const rawLine of configToml.split(/\r?\n/)) {
128
+ const line = stripTomlComment(rawLine).trim();
129
+ if (!line) {
130
+ continue;
131
+ }
132
+ if (line.startsWith("[")) {
133
+ currentProjectPath = parseProjectHeader(line);
134
+ inProjectsTable = line === "[projects]";
135
+ continue;
136
+ }
137
+
138
+ if (currentProjectPath && /^trust_level\s*=\s*["']trusted["']\s*$/.test(line)) {
139
+ trusted.add(currentProjectPath);
140
+ continue;
141
+ }
142
+
143
+ const assignment =
144
+ /^(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?<value>.+)$/.exec(line);
145
+ if (!assignment?.groups) {
146
+ continue;
147
+ }
148
+
149
+ const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key;
150
+ const value = assignment.groups.value.trim();
151
+ if (inProjectsTable && /^\{.*\}$/.test(value)) {
152
+ if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) {
153
+ trusted.add(key);
154
+ }
155
+ continue;
156
+ }
157
+ if (key === "projects" || inProjectsTable) {
158
+ for (const projectPath of parseTrustedInlineProjectEntries(value)) {
159
+ trusted.add(projectPath);
160
+ }
161
+ }
162
+ }
163
+
164
+ return Array.from(trusted);
165
+ }
166
+
167
+ const INHERITED_TOP_LEVEL_CODEX_CONFIG_KEYS = new Set([
168
+ "model",
169
+ "model_provider",
170
+ "model_reasoning_effort",
171
+ "sandbox_mode",
172
+ ]);
173
+
174
+ const INHERITED_MODEL_PROVIDER_CONFIG_KEYS = new Set([
175
+ "name",
176
+ "base_url",
177
+ "wire_api",
178
+ "env_key",
179
+ "env_key_instructions",
180
+ "requires_openai_auth",
181
+ "request_max_retries",
182
+ "stream_max_retries",
183
+ "stream_idle_timeout_ms",
184
+ ]);
185
+
186
+ function parseTableHeader(line: string): string[] | undefined {
187
+ const trimmed = line.trim();
188
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) {
189
+ return undefined;
190
+ }
191
+ return parseTomlDottedKey(trimmed.slice(1, -1));
192
+ }
193
+
194
+ function isInheritedModelProviderTable(parts: string[] | undefined): boolean {
195
+ return parts?.[0] === "model_providers" && parts.length === 2;
196
+ }
197
+
198
+ function parseTopLevelAssignmentKey(line: string): string | undefined {
199
+ const assignment = /^(?<key>[A-Za-z0-9_-]+)\s*=\s*(?<value>.+)$/.exec(line);
200
+ return assignment?.groups?.key;
201
+ }
202
+
203
+ function extractInheritedCodexRuntimeConfig(configToml: string): string {
204
+ const inheritedLines: string[] = [];
205
+ let inAnyTable = false;
206
+ let inInheritedTable = false;
207
+ let pendingInheritedTableHeader = "";
208
+
209
+ function flushInheritedTableHeader(): void {
210
+ if (!pendingInheritedTableHeader) {
211
+ return;
212
+ }
213
+ if (inheritedLines.length > 0 && inheritedLines[inheritedLines.length - 1] !== "") {
214
+ inheritedLines.push("");
215
+ }
216
+ inheritedLines.push(pendingInheritedTableHeader);
217
+ pendingInheritedTableHeader = "";
218
+ }
219
+
220
+ for (const rawLine of configToml.split(/\r?\n/)) {
221
+ const trimmedLine = rawLine.trim();
222
+ const semanticLine = stripTomlComment(rawLine).trim();
223
+
224
+ if (trimmedLine.startsWith("[")) {
225
+ const tableParts = parseTableHeader(trimmedLine);
226
+ inAnyTable = true;
227
+ inInheritedTable = isInheritedModelProviderTable(tableParts);
228
+ if (inInheritedTable) {
229
+ pendingInheritedTableHeader = rawLine.trimEnd();
230
+ } else {
231
+ pendingInheritedTableHeader = "";
232
+ }
233
+ continue;
234
+ }
235
+
236
+ if (inInheritedTable) {
237
+ if (!semanticLine) {
238
+ continue;
239
+ }
240
+ const key = parseTopLevelAssignmentKey(semanticLine);
241
+ if (!key || !INHERITED_MODEL_PROVIDER_CONFIG_KEYS.has(key)) {
242
+ continue;
243
+ }
244
+ flushInheritedTableHeader();
245
+ inheritedLines.push(rawLine.trimEnd());
246
+ continue;
247
+ }
248
+
249
+ if (inAnyTable) {
250
+ continue;
251
+ }
252
+
253
+ const key = parseTopLevelAssignmentKey(semanticLine);
254
+ if (!key) {
255
+ continue;
256
+ }
257
+ if (!INHERITED_TOP_LEVEL_CODEX_CONFIG_KEYS.has(key)) {
258
+ continue;
259
+ }
260
+ inheritedLines.push(rawLine.trimEnd());
261
+ }
262
+
263
+ while (inheritedLines.length > 0 && inheritedLines[inheritedLines.length - 1] === "") {
264
+ inheritedLines.pop();
265
+ }
266
+ return inheritedLines.join("\n");
267
+ }
268
+
269
+ /** Render a session-local Codex config with inherited runtime settings and trust entries. */
270
+ export function renderIsolatedCodexConfig(params: {
271
+ sourceConfigToml?: string;
272
+ projectPaths: string[];
273
+ }): string {
274
+ const normalized = Array.from(
275
+ new Set(
276
+ params.projectPaths
277
+ .map((projectPath) => projectPath.trim())
278
+ .filter(Boolean)
279
+ .map((projectPath) => path.resolve(projectPath)),
280
+ ),
281
+ ).toSorted((left, right) => left.localeCompare(right));
282
+
283
+ const inheritedConfig = params.sourceConfigToml
284
+ ? extractInheritedCodexRuntimeConfig(params.sourceConfigToml)
285
+ : "";
286
+
287
+ return [
288
+ "# Generated by ACTAgent for Codex ACP sessions.",
289
+ inheritedConfig,
290
+ ...normalized.flatMap((projectPath) => [
291
+ "",
292
+ `[projects.${JSON.stringify(projectPath)}]`,
293
+ 'trust_level = "trusted"',
294
+ ]),
295
+ "",
296
+ ]
297
+ .filter((line, index, lines) => !(line === "" && lines[index - 1] === ""))
298
+ .join("\n");
299
+ }
300
+
301
+ /** Render only the project trust section for a session-local Codex config. */
302
+ export function renderIsolatedCodexProjectTrustConfig(projectPaths: string[]): string {
303
+ return renderIsolatedCodexConfig({ projectPaths });
304
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Small shell-command helpers for ACPX-launched processes. Splitting supports
3
+ * simple quoted command strings from config without invoking a shell parser.
4
+ */
5
+ /** Quote one command argument for display or config serialization. */
6
+ export function quoteCommandPart(value: string): string {
7
+ return JSON.stringify(value);
8
+ }
9
+
10
+ /** Split a command string into argv-like parts using simple quote/backslash rules. */
11
+ export function splitCommandParts(value: string): string[] {
12
+ const parts: string[] = [];
13
+ let current = "";
14
+ let quote: "'" | '"' | null = null;
15
+ let escaping = false;
16
+
17
+ for (const ch of value) {
18
+ if (escaping) {
19
+ current += ch;
20
+ escaping = false;
21
+ continue;
22
+ }
23
+ if (ch === "\\" && quote !== "'") {
24
+ escaping = true;
25
+ continue;
26
+ }
27
+ if (quote) {
28
+ if (ch === quote) {
29
+ quote = null;
30
+ } else {
31
+ current += ch;
32
+ }
33
+ continue;
34
+ }
35
+ if (ch === "'" || ch === '"') {
36
+ quote = ch;
37
+ continue;
38
+ }
39
+ if (/\s/.test(ch)) {
40
+ if (current) {
41
+ parts.push(current);
42
+ current = "";
43
+ }
44
+ continue;
45
+ }
46
+ current += ch;
47
+ }
48
+
49
+ if (escaping) {
50
+ current += "\\";
51
+ }
52
+ if (current) {
53
+ parts.push(current);
54
+ }
55
+ return parts;
56
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ACPX plugin configuration schema and public config types. Runtime setup uses
3
+ * this file as the single source of truth for validation and defaulting.
4
+ */
5
+ import { z } from "zod";
6
+
7
+ const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const;
8
+ /** Permission policy applied to interactive ACPX tool requests. */
9
+ export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
10
+
11
+ const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
12
+ /** Permission policy applied when ACPX cannot ask a human for approval. */
13
+ export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
14
+
15
+ /** Default session timeout for ACPX runtime turns. */
16
+ export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
17
+
18
+ /** Raw MCP server command config accepted from plugin configuration. */
19
+ export type McpServerConfig = {
20
+ command: string;
21
+ args?: string[];
22
+ env?: Record<string, string>;
23
+ };
24
+
25
+ /** Normalized MCP server config emitted to the ACPX runtime process. */
26
+ export type AcpxMcpServer = {
27
+ name: string;
28
+ command: string;
29
+ args: string[];
30
+ env: Array<{ name: string; value: string }>;
31
+ };
32
+
33
+ /** User-provided ACPX plugin configuration before defaults are resolved. */
34
+ export type AcpxPluginConfig = {
35
+ cwd?: string;
36
+ stateDir?: string;
37
+ probeAgent?: string;
38
+ permissionMode?: AcpxPermissionMode;
39
+ nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy;
40
+ pluginToolsMcpBridge?: boolean;
41
+ actAgentToolsMcpBridge?: boolean;
42
+ strictWindowsCmdWrapper?: boolean;
43
+ timeoutSeconds?: number;
44
+ queueOwnerTtlSeconds?: number;
45
+ mcpServers?: Record<string, McpServerConfig>;
46
+ agents?: Record<string, { command: string; args?: string[] }>;
47
+ };
48
+
49
+ /** Fully resolved ACPX config consumed by the runtime service. */
50
+ export type ResolvedAcpxPluginConfig = {
51
+ cwd: string;
52
+ stateDir: string;
53
+ probeAgent?: string;
54
+ permissionMode: AcpxPermissionMode;
55
+ nonInteractivePermissions: AcpxNonInteractivePermissionPolicy;
56
+ pluginToolsMcpBridge: boolean;
57
+ actAgentToolsMcpBridge: boolean;
58
+ strictWindowsCmdWrapper: boolean;
59
+ timeoutSeconds?: number;
60
+ queueOwnerTtlSeconds: number;
61
+ legacyCompatibilityConfig: {
62
+ strictWindowsCmdWrapper?: boolean;
63
+ queueOwnerTtlSeconds?: number;
64
+ };
65
+ mcpServers: Record<string, McpServerConfig>;
66
+ agents: Record<string, string>;
67
+ };
68
+
69
+ const nonEmptyTrimmedString = (message: string) =>
70
+ z.string({ error: message }).trim().min(1, { error: message });
71
+
72
+ const McpServerConfigSchema = z.object({
73
+ command: nonEmptyTrimmedString("command must be a non-empty string").describe(
74
+ "Command to run the MCP server",
75
+ ),
76
+ args: z
77
+ .array(z.string({ error: "args must be an array of strings" }), {
78
+ error: "args must be an array of strings",
79
+ })
80
+ .optional()
81
+ .describe("Arguments to pass to the command"),
82
+ env: z
83
+ .record(z.string(), z.string({ error: "env values must be strings" }), {
84
+ error: "env must be an object of strings",
85
+ })
86
+ .optional()
87
+ .describe("Environment variables for the MCP server"),
88
+ });
89
+
90
+ /** Zod schema for validating raw ACPX plugin config from ACTAgent config. */
91
+ export const AcpxPluginConfigSchema = z.strictObject({
92
+ cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(),
93
+ stateDir: nonEmptyTrimmedString("stateDir must be a non-empty string").optional(),
94
+ probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(),
95
+ permissionMode: z
96
+ .enum(ACPX_PERMISSION_MODES, {
97
+ error: `permissionMode must be one of: ${ACPX_PERMISSION_MODES.join(", ")}`,
98
+ })
99
+ .optional(),
100
+ nonInteractivePermissions: z
101
+ .enum(ACPX_NON_INTERACTIVE_POLICIES, {
102
+ error: `nonInteractivePermissions must be one of: ${ACPX_NON_INTERACTIVE_POLICIES.join(", ")}`,
103
+ })
104
+ .optional(),
105
+ pluginToolsMcpBridge: z.boolean({ error: "pluginToolsMcpBridge must be a boolean" }).optional(),
106
+ actAgentToolsMcpBridge: z
107
+ .boolean({ error: "actAgentToolsMcpBridge must be a boolean" })
108
+ .optional(),
109
+ strictWindowsCmdWrapper: z
110
+ .boolean({ error: "strictWindowsCmdWrapper must be a boolean" })
111
+ .optional(),
112
+ timeoutSeconds: z
113
+ .number({ error: "timeoutSeconds must be a number >= 0.001" })
114
+ .min(0.001, { error: "timeoutSeconds must be a number >= 0.001" })
115
+ .default(DEFAULT_ACPX_TIMEOUT_SECONDS),
116
+ queueOwnerTtlSeconds: z
117
+ .number({ error: "queueOwnerTtlSeconds must be a number >= 0" })
118
+ .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" })
119
+ .optional(),
120
+ mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
121
+ agents: z
122
+ .record(
123
+ z.string(),
124
+ z.strictObject({
125
+ command: nonEmptyTrimmedString("agents.<id>.command must be a non-empty string"),
126
+ args: z.array(z.string({ error: "args must be an array of strings" })).optional(),
127
+ }),
128
+ )
129
+ .optional(),
130
+ });