@nathapp/nax 0.42.9 → 0.43.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.
- package/CHANGELOG.md +19 -0
- package/bin/nax.ts +11 -1
- package/dist/nax.js +89 -31
- package/package.json +1 -1
- package/src/acceptance/fix-generator.ts +1 -0
- package/src/acceptance/generator.ts +2 -1
- package/src/acceptance/refinement.ts +1 -0
- package/src/agents/acp/adapter.ts +19 -9
- package/src/agents/acp/spawn-client.ts +2 -2
- package/src/agents/claude-complete.ts +6 -0
- package/src/agents/claude-execution.ts +2 -1
- package/src/agents/claude-plan.ts +6 -2
- package/src/agents/claude.ts +6 -1
- package/src/agents/types.ts +10 -0
- package/src/analyze/classifier.ts +1 -0
- package/src/cli/analyze.ts +1 -1
- package/src/cli/plan.ts +5 -5
- package/src/config/permissions.ts +63 -0
- package/src/config/runtime-types.ts +11 -0
- package/src/config/schemas.ts +14 -0
- package/src/interaction/plugins/auto.ts +1 -0
- package/src/pipeline/stages/execution.ts +4 -1
- package/src/pipeline/stages/routing.ts +1 -1
- package/src/routing/strategies/llm.ts +1 -1
- package/src/tdd/rectification-gate.ts +4 -1
- package/src/tdd/session-runner.ts +4 -1
- package/src/verification/rectification-loop.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.43.0] - 2026-03-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **PERM-001:** `src/config/permissions.ts` — `resolvePermissions(config, stage)` as the single source of truth for all permission decisions across CLI and ACP adapters.
|
|
12
|
+
- **New types:** `PermissionProfile` (`"unrestricted" | "safe" | "scoped"`), `PipelineStage`, `ResolvedPermissions` interface.
|
|
13
|
+
- **Schema:** `execution.permissionProfile` config field — takes precedence over legacy `dangerouslySkipPermissions` boolean. `"scoped"` is a Phase 2 stub.
|
|
14
|
+
- **`pipelineStage?`** added to `AgentRunOptions` — each call site sets the appropriate stage (`"plan"`, `"run"`, `"rectification"`, etc.).
|
|
15
|
+
- **`config?`** added to `CompleteOptions` — all `complete()` call sites now thread config so permissions are resolved correctly.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Hardcoded `--dangerously-skip-permissions`** in `claude-plan.ts` — now resolved from config.
|
|
19
|
+
- **`?? false` fallback** in `plan.ts` — removed; replaced with `resolvePermissions()`.
|
|
20
|
+
- **`?? true` fallback** in `claude-execution.ts` — removed; replaced with `resolvePermissions()`.
|
|
21
|
+
- **`resolvePermissions(undefined, ...)` in ACP `complete()`** — now passes `_options?.config`.
|
|
22
|
+
- All ACP adapter permission ternaries replaced with `resolvePermissions()`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `nax/config.json` — explicit `"permissionProfile": "unrestricted"` (was implicit via schema default).
|
|
26
|
+
|
|
8
27
|
## [0.30.0] - 2026-03-08
|
|
9
28
|
|
|
10
29
|
### Fixed
|
package/bin/nax.ts
CHANGED
|
@@ -69,7 +69,7 @@ import { unlockCommand } from "../src/commands/unlock";
|
|
|
69
69
|
import { DEFAULT_CONFIG, findProjectDir, loadConfig, validateDirectory } from "../src/config";
|
|
70
70
|
import { run } from "../src/execution";
|
|
71
71
|
import { loadHooksConfig } from "../src/hooks";
|
|
72
|
-
import { type LogLevel, initLogger } from "../src/logger";
|
|
72
|
+
import { type LogLevel, initLogger, resetLogger } from "../src/logger";
|
|
73
73
|
import { countStories, loadPRD } from "../src/prd";
|
|
74
74
|
import { PipelineEventEmitter, type StoryDisplayState, renderTui } from "../src/tui";
|
|
75
75
|
import { NAX_VERSION } from "../src/version";
|
|
@@ -344,6 +344,13 @@ program
|
|
|
344
344
|
// Run plan phase if --plan flag is set (AC-4: runs plan then execute)
|
|
345
345
|
if (options.plan && options.from) {
|
|
346
346
|
try {
|
|
347
|
+
// Initialize plan logger before calling planCommand — writes to features/<feature>/plan-<ts>.jsonl
|
|
348
|
+
mkdirSync(featureDir, { recursive: true });
|
|
349
|
+
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
350
|
+
const planLogPath = join(featureDir, `plan-${planLogId}.jsonl`);
|
|
351
|
+
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
352
|
+
console.log(chalk.dim(` [Plan log: ${planLogPath}]`));
|
|
353
|
+
|
|
347
354
|
console.log(chalk.dim(" [Planning phase: generating PRD from spec]"));
|
|
348
355
|
const generatedPrdPath = await planCommand(workdir, config, {
|
|
349
356
|
from: options.from,
|
|
@@ -391,6 +398,9 @@ program
|
|
|
391
398
|
process.exit(1);
|
|
392
399
|
}
|
|
393
400
|
|
|
401
|
+
// Reset plan logger (if plan phase ran) so the run logger can be initialized fresh
|
|
402
|
+
resetLogger();
|
|
403
|
+
|
|
394
404
|
// Create run directory and JSONL log file path
|
|
395
405
|
const runsDir = join(featureDir, "runs");
|
|
396
406
|
mkdirSync(runsDir, { recursive: true });
|
package/dist/nax.js
CHANGED
|
@@ -2552,6 +2552,24 @@ var require_commander = __commonJS((exports) => {
|
|
|
2552
2552
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
2553
2553
|
});
|
|
2554
2554
|
|
|
2555
|
+
// src/config/permissions.ts
|
|
2556
|
+
function resolvePermissions(config, _stage) {
|
|
2557
|
+
const profile = config?.execution?.permissionProfile ?? (config?.execution?.dangerouslySkipPermissions ? "unrestricted" : "safe");
|
|
2558
|
+
switch (profile) {
|
|
2559
|
+
case "unrestricted":
|
|
2560
|
+
return { mode: "approve-all", skipPermissions: true };
|
|
2561
|
+
case "safe":
|
|
2562
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
2563
|
+
case "scoped":
|
|
2564
|
+
return resolveScopedPermissions(config, _stage);
|
|
2565
|
+
default:
|
|
2566
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
function resolveScopedPermissions(_config, _stage) {
|
|
2570
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2555
2573
|
// src/logging/types.ts
|
|
2556
2574
|
var EMOJI;
|
|
2557
2575
|
var init_types = __esm(() => {
|
|
@@ -3244,6 +3262,10 @@ async function executeComplete(binary, prompt, options) {
|
|
|
3244
3262
|
if (options?.jsonMode) {
|
|
3245
3263
|
cmd.push("--output-format", "json");
|
|
3246
3264
|
}
|
|
3265
|
+
const { skipPermissions } = resolvePermissions(options?.config, "complete");
|
|
3266
|
+
if (skipPermissions) {
|
|
3267
|
+
cmd.push("--dangerously-skip-permissions");
|
|
3268
|
+
}
|
|
3247
3269
|
const spawnOpts = { stdout: "pipe", stderr: "pipe" };
|
|
3248
3270
|
if (options?.workdir)
|
|
3249
3271
|
spawnOpts.cwd = options.workdir;
|
|
@@ -3501,7 +3523,7 @@ var init_cost = __esm(() => {
|
|
|
3501
3523
|
// src/agents/claude-execution.ts
|
|
3502
3524
|
function buildCommand(binary, options) {
|
|
3503
3525
|
const model = options.modelDef.model;
|
|
3504
|
-
const skipPermissions = options.
|
|
3526
|
+
const { skipPermissions } = resolvePermissions(options.config, options.pipelineStage ?? "run");
|
|
3505
3527
|
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
3506
3528
|
return [binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
3507
3529
|
}
|
|
@@ -17619,6 +17641,12 @@ var init_schemas3 = __esm(() => {
|
|
|
17619
17641
|
lintCommand: exports_external.string().nullable().optional(),
|
|
17620
17642
|
typecheckCommand: exports_external.string().nullable().optional(),
|
|
17621
17643
|
dangerouslySkipPermissions: exports_external.boolean().default(true),
|
|
17644
|
+
permissionProfile: exports_external.enum(["unrestricted", "safe", "scoped"]).optional(),
|
|
17645
|
+
permissions: exports_external.record(exports_external.string(), exports_external.object({
|
|
17646
|
+
mode: exports_external.enum(["approve-all", "approve-reads", "scoped"]),
|
|
17647
|
+
allowedTools: exports_external.array(exports_external.string()).optional(),
|
|
17648
|
+
inherit: exports_external.string().optional()
|
|
17649
|
+
})).optional(),
|
|
17622
17650
|
smartTestRunner: smartTestRunnerFieldSchema
|
|
17623
17651
|
});
|
|
17624
17652
|
QualityConfigSchema = exports_external.object({
|
|
@@ -18104,7 +18132,10 @@ function buildPlanCommand(binary, options) {
|
|
|
18104
18132
|
if (modelDef) {
|
|
18105
18133
|
cmd.push("--model", modelDef.model);
|
|
18106
18134
|
}
|
|
18107
|
-
|
|
18135
|
+
const { skipPermissions } = resolvePermissions(options.config, "plan");
|
|
18136
|
+
if (skipPermissions) {
|
|
18137
|
+
cmd.push("--dangerously-skip-permissions");
|
|
18138
|
+
}
|
|
18108
18139
|
let fullPrompt = options.prompt;
|
|
18109
18140
|
if (options.codebaseContext) {
|
|
18110
18141
|
fullPrompt = `${options.codebaseContext}
|
|
@@ -18306,7 +18337,11 @@ class ClaudeCodeAdapter {
|
|
|
18306
18337
|
}
|
|
18307
18338
|
modelDef = resolveBalancedModelDef2(options.config);
|
|
18308
18339
|
}
|
|
18309
|
-
const
|
|
18340
|
+
const { skipPermissions } = resolvePermissions(options.config, "run");
|
|
18341
|
+
const cmd = [this.binary, "--model", modelDef.model, "-p", prompt];
|
|
18342
|
+
if (skipPermissions) {
|
|
18343
|
+
cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
|
|
18344
|
+
}
|
|
18310
18345
|
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
18311
18346
|
const proc = _decomposeDeps.spawn(cmd, {
|
|
18312
18347
|
cwd: options.workdir,
|
|
@@ -18497,7 +18532,8 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
18497
18532
|
response = await _refineDeps.adapter.complete(prompt, {
|
|
18498
18533
|
jsonMode: true,
|
|
18499
18534
|
maxTokens: 4096,
|
|
18500
|
-
model: modelDef.model
|
|
18535
|
+
model: modelDef.model,
|
|
18536
|
+
config: config2
|
|
18501
18537
|
});
|
|
18502
18538
|
} catch (error48) {
|
|
18503
18539
|
const reason = errorMessage(error48);
|
|
@@ -18579,7 +18615,7 @@ describe("${options.featureName} - Acceptance Tests", () => {
|
|
|
18579
18615
|
|
|
18580
18616
|
Respond with ONLY the TypeScript test code (no markdown code fences, no explanation).`;
|
|
18581
18617
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
18582
|
-
const testCode = await _generatorPRDDeps.adapter.complete(prompt);
|
|
18618
|
+
const testCode = await _generatorPRDDeps.adapter.complete(prompt, { config: options.config });
|
|
18583
18619
|
const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
|
|
18584
18620
|
acId: `AC-${i + 1}`,
|
|
18585
18621
|
original: c.original,
|
|
@@ -18702,7 +18738,8 @@ async function generateAcceptanceTests(adapter, options) {
|
|
|
18702
18738
|
const prompt = buildAcceptanceTestPrompt(criteria, options.featureName, options.codebaseContext);
|
|
18703
18739
|
try {
|
|
18704
18740
|
const output = await adapter.complete(prompt, {
|
|
18705
|
-
model: options.modelDef.model
|
|
18741
|
+
model: options.modelDef.model,
|
|
18742
|
+
config: options.config
|
|
18706
18743
|
});
|
|
18707
18744
|
const testCode = extractTestCode(output);
|
|
18708
18745
|
return {
|
|
@@ -18823,7 +18860,8 @@ async function generateFixStories(adapter, options) {
|
|
|
18823
18860
|
const prompt = buildFixPrompt(failedAC, acText, testOutput, relatedStories, prd);
|
|
18824
18861
|
try {
|
|
18825
18862
|
const fixDescription = await adapter.complete(prompt, {
|
|
18826
|
-
model: modelDef.model
|
|
18863
|
+
model: modelDef.model,
|
|
18864
|
+
config: options.config
|
|
18827
18865
|
});
|
|
18828
18866
|
fixStories.push({
|
|
18829
18867
|
id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
|
|
@@ -19120,7 +19158,7 @@ class SpawnAcpClient {
|
|
|
19120
19158
|
pidRegistry: this.pidRegistry
|
|
19121
19159
|
});
|
|
19122
19160
|
}
|
|
19123
|
-
async loadSession(sessionName, agentName) {
|
|
19161
|
+
async loadSession(sessionName, agentName, permissionMode) {
|
|
19124
19162
|
const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
|
|
19125
19163
|
const proc = _spawnClientDeps.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
19126
19164
|
const exitCode = await proc.exited;
|
|
@@ -19133,7 +19171,7 @@ class SpawnAcpClient {
|
|
|
19133
19171
|
cwd: this.cwd,
|
|
19134
19172
|
model: this.model,
|
|
19135
19173
|
timeoutSeconds: this.timeoutSeconds,
|
|
19136
|
-
permissionMode
|
|
19174
|
+
permissionMode,
|
|
19137
19175
|
env: this.env,
|
|
19138
19176
|
pidRegistry: this.pidRegistry
|
|
19139
19177
|
});
|
|
@@ -19224,7 +19262,7 @@ async function ensureAcpSession(client, sessionName, agentName, permissionMode)
|
|
|
19224
19262
|
}
|
|
19225
19263
|
if (client.loadSession) {
|
|
19226
19264
|
try {
|
|
19227
|
-
const existing = await client.loadSession(sessionName, agentName);
|
|
19265
|
+
const existing = await client.loadSession(sessionName, agentName, permissionMode);
|
|
19228
19266
|
if (existing) {
|
|
19229
19267
|
getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
|
|
19230
19268
|
return existing;
|
|
@@ -19409,11 +19447,11 @@ class AcpAgentAdapter {
|
|
|
19409
19447
|
sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
|
|
19410
19448
|
}
|
|
19411
19449
|
sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
|
|
19412
|
-
const
|
|
19450
|
+
const resolvedPerm = resolvePermissions(options.config, options.pipelineStage ?? "run");
|
|
19451
|
+
const permissionMode = resolvedPerm.mode;
|
|
19413
19452
|
getSafeLogger()?.info("acp-adapter", "Permission mode resolved", {
|
|
19414
19453
|
permission: permissionMode,
|
|
19415
|
-
|
|
19416
|
-
stage: options.featureName ? "run" : "plan"
|
|
19454
|
+
stage: options.pipelineStage ?? "run"
|
|
19417
19455
|
});
|
|
19418
19456
|
const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
|
|
19419
19457
|
if (options.featureName && options.storyId) {
|
|
@@ -19494,7 +19532,7 @@ class AcpAgentAdapter {
|
|
|
19494
19532
|
async complete(prompt, _options) {
|
|
19495
19533
|
const model = _options?.model ?? "default";
|
|
19496
19534
|
const timeoutMs = _options?.timeoutMs ?? 120000;
|
|
19497
|
-
const permissionMode = _options?.
|
|
19535
|
+
const permissionMode = resolvePermissions(_options?.config, "complete").mode;
|
|
19498
19536
|
const workdir = _options?.workdir;
|
|
19499
19537
|
let lastError;
|
|
19500
19538
|
for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
@@ -19574,7 +19612,9 @@ class AcpAgentAdapter {
|
|
|
19574
19612
|
modelTier: options.modelTier ?? "balanced",
|
|
19575
19613
|
modelDef,
|
|
19576
19614
|
timeoutSeconds,
|
|
19577
|
-
dangerouslySkipPermissions: options.
|
|
19615
|
+
dangerouslySkipPermissions: resolvePermissions(options.config, "plan").skipPermissions,
|
|
19616
|
+
pipelineStage: "plan",
|
|
19617
|
+
config: options.config,
|
|
19578
19618
|
interactionBridge: options.interactionBridge,
|
|
19579
19619
|
maxInteractionTurns: options.maxInteractionTurns,
|
|
19580
19620
|
featureName: options.featureName,
|
|
@@ -19596,7 +19636,11 @@ class AcpAgentAdapter {
|
|
|
19596
19636
|
const prompt = buildDecomposePrompt(options);
|
|
19597
19637
|
let output;
|
|
19598
19638
|
try {
|
|
19599
|
-
output = await this.complete(prompt, {
|
|
19639
|
+
output = await this.complete(prompt, {
|
|
19640
|
+
model,
|
|
19641
|
+
jsonMode: true,
|
|
19642
|
+
config: options.config
|
|
19643
|
+
});
|
|
19600
19644
|
} catch (err) {
|
|
19601
19645
|
const msg = err instanceof Error ? err.message : String(err);
|
|
19602
19646
|
throw new Error(`[acp-adapter] decompose() failed: ${msg}`, { cause: err });
|
|
@@ -20854,7 +20898,7 @@ async function callLlmOnce(adapter, modelTier, prompt, config2, timeoutMs) {
|
|
|
20854
20898
|
}, timeoutMs);
|
|
20855
20899
|
});
|
|
20856
20900
|
timeoutPromise.catch(() => {});
|
|
20857
|
-
const outputPromise = adapter.complete(prompt, { model: modelArg });
|
|
20901
|
+
const outputPromise = adapter.complete(prompt, { model: modelArg, config: config2 });
|
|
20858
20902
|
try {
|
|
20859
20903
|
const result = await Promise.race([outputPromise, timeoutPromise]);
|
|
20860
20904
|
clearTimeout(timeoutId);
|
|
@@ -21924,7 +21968,7 @@ var package_default;
|
|
|
21924
21968
|
var init_package = __esm(() => {
|
|
21925
21969
|
package_default = {
|
|
21926
21970
|
name: "@nathapp/nax",
|
|
21927
|
-
version: "0.
|
|
21971
|
+
version: "0.43.1",
|
|
21928
21972
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
21929
21973
|
type: "module",
|
|
21930
21974
|
bin: {
|
|
@@ -21997,8 +22041,8 @@ var init_version = __esm(() => {
|
|
|
21997
22041
|
NAX_VERSION = package_default.version;
|
|
21998
22042
|
NAX_COMMIT = (() => {
|
|
21999
22043
|
try {
|
|
22000
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22001
|
-
return "
|
|
22044
|
+
if (/^[0-9a-f]{6,10}$/.test("82a45aa"))
|
|
22045
|
+
return "82a45aa";
|
|
22002
22046
|
} catch {}
|
|
22003
22047
|
try {
|
|
22004
22048
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -23230,7 +23274,8 @@ class AutoInteractionPlugin {
|
|
|
23230
23274
|
}
|
|
23231
23275
|
const output = await adapter.complete(prompt, {
|
|
23232
23276
|
...modelArg && { model: modelArg },
|
|
23233
|
-
jsonMode: true
|
|
23277
|
+
jsonMode: true,
|
|
23278
|
+
...this.config.naxConfig && { config: this.config.naxConfig }
|
|
23234
23279
|
});
|
|
23235
23280
|
return this.parseResponse(output);
|
|
23236
23281
|
}
|
|
@@ -26315,7 +26360,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
|
|
|
26315
26360
|
modelTier: implementerTier,
|
|
26316
26361
|
modelDef: resolveModel(config2.models[implementerTier]),
|
|
26317
26362
|
timeoutSeconds: config2.execution.sessionTimeoutSeconds,
|
|
26318
|
-
dangerouslySkipPermissions: config2.
|
|
26363
|
+
dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
|
|
26364
|
+
pipelineStage: "rectification",
|
|
26365
|
+
config: config2,
|
|
26319
26366
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
26320
26367
|
featureName,
|
|
26321
26368
|
storyId: story.id,
|
|
@@ -26923,7 +26970,9 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
|
|
|
26923
26970
|
modelTier,
|
|
26924
26971
|
modelDef: resolveModel(config2.models[modelTier]),
|
|
26925
26972
|
timeoutSeconds: config2.execution.sessionTimeoutSeconds,
|
|
26926
|
-
dangerouslySkipPermissions: config2.
|
|
26973
|
+
dangerouslySkipPermissions: resolvePermissions(config2, "run").skipPermissions,
|
|
26974
|
+
pipelineStage: "run",
|
|
26975
|
+
config: config2,
|
|
26927
26976
|
maxInteractionTurns: config2.agent?.maxInteractionTurns,
|
|
26928
26977
|
featureName,
|
|
26929
26978
|
storyId: story.id,
|
|
@@ -27673,7 +27722,9 @@ Category: ${tddResult.failureCategory ?? "unknown"}`,
|
|
|
27673
27722
|
modelTier: ctx.routing.modelTier,
|
|
27674
27723
|
modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
|
|
27675
27724
|
timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
|
|
27676
|
-
dangerouslySkipPermissions: ctx.config.
|
|
27725
|
+
dangerouslySkipPermissions: resolvePermissions(ctx.config, "run").skipPermissions,
|
|
27726
|
+
pipelineStage: "run",
|
|
27727
|
+
config: ctx.config,
|
|
27677
27728
|
maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
|
|
27678
27729
|
pidRegistry: ctx.pidRegistry,
|
|
27679
27730
|
featureName: ctx.prd.feature,
|
|
@@ -28223,7 +28274,9 @@ ${rectificationPrompt}`;
|
|
|
28223
28274
|
modelTier,
|
|
28224
28275
|
modelDef,
|
|
28225
28276
|
timeoutSeconds: config2.execution.sessionTimeoutSeconds,
|
|
28226
|
-
dangerouslySkipPermissions: config2.
|
|
28277
|
+
dangerouslySkipPermissions: resolvePermissions(config2, "rectification").skipPermissions,
|
|
28278
|
+
pipelineStage: "rectification",
|
|
28279
|
+
config: config2,
|
|
28227
28280
|
maxInteractionTurns: config2.agent?.maxInteractionTurns
|
|
28228
28281
|
});
|
|
28229
28282
|
if (agentResult.success) {
|
|
@@ -28944,7 +28997,7 @@ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
|
|
|
28944
28997
|
}
|
|
28945
28998
|
const adapter = {
|
|
28946
28999
|
async decompose(prompt) {
|
|
28947
|
-
return agent.complete(prompt, { jsonMode: true });
|
|
29000
|
+
return agent.complete(prompt, { jsonMode: true, config: config2 });
|
|
28948
29001
|
}
|
|
28949
29002
|
};
|
|
28950
29003
|
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
|
@@ -65845,7 +65898,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
65845
65898
|
const cliAdapter = _deps2.getAgent(agentName);
|
|
65846
65899
|
if (!cliAdapter)
|
|
65847
65900
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65848
|
-
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
|
|
65901
|
+
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir, config: config2 });
|
|
65849
65902
|
try {
|
|
65850
65903
|
const envelope = JSON.parse(rawResponse);
|
|
65851
65904
|
if (envelope?.type === "result" && typeof envelope?.result === "string") {
|
|
@@ -65859,13 +65912,12 @@ async function planCommand(workdir, config2, options) {
|
|
|
65859
65912
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
65860
65913
|
const interactionBridge = createCliInteractionBridge();
|
|
65861
65914
|
const pidRegistry = new PidRegistry(workdir);
|
|
65862
|
-
const
|
|
65863
|
-
const permissionMode = dangerouslySkipPermissions ? "approve-all" : "approve-reads";
|
|
65915
|
+
const resolvedPerm = resolvePermissions(config2, "plan");
|
|
65864
65916
|
const resolvedModel = config2?.plan?.model ?? "balanced";
|
|
65865
65917
|
logger?.info("plan", "Starting interactive planning session", {
|
|
65866
65918
|
agent: agentName,
|
|
65867
65919
|
model: resolvedModel,
|
|
65868
|
-
permission:
|
|
65920
|
+
permission: resolvedPerm.mode,
|
|
65869
65921
|
workdir,
|
|
65870
65922
|
feature: options.feature,
|
|
65871
65923
|
timeoutSeconds
|
|
@@ -65880,7 +65932,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
65880
65932
|
interactionBridge,
|
|
65881
65933
|
config: config2,
|
|
65882
65934
|
modelTier: resolvedModel,
|
|
65883
|
-
dangerouslySkipPermissions,
|
|
65935
|
+
dangerouslySkipPermissions: resolvedPerm.skipPermissions,
|
|
65884
65936
|
maxInteractionTurns: config2?.agent?.maxInteractionTurns,
|
|
65885
65937
|
featureName: options.feature,
|
|
65886
65938
|
pidRegistry
|
|
@@ -76755,6 +76807,11 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76755
76807
|
const prdPath = join43(featureDir, "prd.json");
|
|
76756
76808
|
if (options.plan && options.from) {
|
|
76757
76809
|
try {
|
|
76810
|
+
mkdirSync6(featureDir, { recursive: true });
|
|
76811
|
+
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
76812
|
+
const planLogPath = join43(featureDir, `plan-${planLogId}.jsonl`);
|
|
76813
|
+
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
76814
|
+
console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
|
|
76758
76815
|
console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
|
|
76759
76816
|
const generatedPrdPath = await planCommand(workdir, config2, {
|
|
76760
76817
|
from: options.from,
|
|
@@ -76789,6 +76846,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
76789
76846
|
console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
|
|
76790
76847
|
process.exit(1);
|
|
76791
76848
|
}
|
|
76849
|
+
resetLogger();
|
|
76792
76850
|
const runsDir = join43(featureDir, "runs");
|
|
76793
76851
|
mkdirSync6(runsDir, { recursive: true });
|
|
76794
76852
|
const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
package/package.json
CHANGED
|
@@ -110,7 +110,7 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
|
|
|
110
110
|
|
|
111
111
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
112
112
|
|
|
113
|
-
const testCode = await _generatorPRDDeps.adapter.complete(prompt);
|
|
113
|
+
const testCode = await _generatorPRDDeps.adapter.complete(prompt, { config: options.config });
|
|
114
114
|
|
|
115
115
|
const refinedJsonContent = JSON.stringify(
|
|
116
116
|
refinedCriteria.map((c, i) => ({
|
|
@@ -298,6 +298,7 @@ export async function generateAcceptanceTests(
|
|
|
298
298
|
// Call adapter to generate tests
|
|
299
299
|
const output = await adapter.complete(prompt, {
|
|
300
300
|
model: options.modelDef.model,
|
|
301
|
+
config: options.config,
|
|
301
302
|
});
|
|
302
303
|
|
|
303
304
|
// Extract test code from output
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
16
17
|
import { getSafeLogger } from "../../logger";
|
|
17
18
|
import { buildDecomposePrompt, parseDecomposeOutput } from "../claude-decompose";
|
|
18
19
|
import { createSpawnAcpClient } from "./spawn-client";
|
|
@@ -92,7 +93,7 @@ export interface AcpClient {
|
|
|
92
93
|
start(): Promise<void>;
|
|
93
94
|
createSession(opts: { agentName: string; permissionMode: string; sessionName?: string }): Promise<AcpSession>;
|
|
94
95
|
/** Resume an existing named session. Returns null if the session is not found. */
|
|
95
|
-
loadSession?(sessionName: string, agentName: string): Promise<AcpSession | null>;
|
|
96
|
+
loadSession?(sessionName: string, agentName: string, permissionMode: string): Promise<AcpSession | null>;
|
|
96
97
|
close(): Promise<void>;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -192,7 +193,7 @@ export async function ensureAcpSession(
|
|
|
192
193
|
// Try to resume existing session first
|
|
193
194
|
if (client.loadSession) {
|
|
194
195
|
try {
|
|
195
|
-
const existing = await client.loadSession(sessionName, agentName);
|
|
196
|
+
const existing = await client.loadSession(sessionName, agentName, permissionMode);
|
|
196
197
|
if (existing) {
|
|
197
198
|
getSafeLogger()?.debug("acp-adapter", `Resumed existing session: ${sessionName}`);
|
|
198
199
|
return existing;
|
|
@@ -451,12 +452,12 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
451
452
|
}
|
|
452
453
|
sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
|
|
453
454
|
|
|
454
|
-
// 2.
|
|
455
|
-
const
|
|
455
|
+
// 2. Resolve permission mode from config via single source of truth.
|
|
456
|
+
const resolvedPerm = resolvePermissions(options.config, options.pipelineStage ?? "run");
|
|
457
|
+
const permissionMode = resolvedPerm.mode;
|
|
456
458
|
getSafeLogger()?.info("acp-adapter", "Permission mode resolved", {
|
|
457
459
|
permission: permissionMode,
|
|
458
|
-
|
|
459
|
-
stage: options.featureName ? "run" : "plan",
|
|
460
|
+
stage: options.pipelineStage ?? "run",
|
|
460
461
|
});
|
|
461
462
|
|
|
462
463
|
// 3. Ensure session (resume existing or create new)
|
|
@@ -567,7 +568,7 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
567
568
|
async complete(prompt: string, _options?: CompleteOptions): Promise<string> {
|
|
568
569
|
const model = _options?.model ?? "default";
|
|
569
570
|
const timeoutMs = _options?.timeoutMs ?? 120_000; // 2-min safety net by default
|
|
570
|
-
const permissionMode = _options?.
|
|
571
|
+
const permissionMode = resolvePermissions(_options?.config, "complete").mode;
|
|
571
572
|
const workdir = _options?.workdir;
|
|
572
573
|
|
|
573
574
|
let lastError: Error | undefined;
|
|
@@ -677,7 +678,12 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
677
678
|
modelTier: options.modelTier ?? "balanced",
|
|
678
679
|
modelDef,
|
|
679
680
|
timeoutSeconds,
|
|
680
|
-
dangerouslySkipPermissions:
|
|
681
|
+
dangerouslySkipPermissions: resolvePermissions(
|
|
682
|
+
options.config as import("../../config").NaxConfig | undefined,
|
|
683
|
+
"plan",
|
|
684
|
+
).skipPermissions,
|
|
685
|
+
pipelineStage: "plan",
|
|
686
|
+
config: options.config as import("../../config").NaxConfig | undefined,
|
|
681
687
|
interactionBridge: options.interactionBridge,
|
|
682
688
|
maxInteractionTurns: options.maxInteractionTurns,
|
|
683
689
|
featureName: options.featureName,
|
|
@@ -704,7 +710,11 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
704
710
|
|
|
705
711
|
let output: string;
|
|
706
712
|
try {
|
|
707
|
-
output = await this.complete(prompt, {
|
|
713
|
+
output = await this.complete(prompt, {
|
|
714
|
+
model,
|
|
715
|
+
jsonMode: true,
|
|
716
|
+
config: options.config as import("../../config").NaxConfig | undefined,
|
|
717
|
+
});
|
|
708
718
|
} catch (err) {
|
|
709
719
|
const msg = err instanceof Error ? err.message : String(err);
|
|
710
720
|
throw new Error(`[acp-adapter] decompose() failed: ${msg}`, { cause: err });
|
|
@@ -316,7 +316,7 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
async loadSession(sessionName: string, agentName: string): Promise<AcpSession | null> {
|
|
319
|
+
async loadSession(sessionName: string, agentName: string, permissionMode: string): Promise<AcpSession | null> {
|
|
320
320
|
// Try to ensure session exists — if it does, acpx returns success
|
|
321
321
|
const cmd = ["acpx", "--cwd", this.cwd, agentName, "sessions", "ensure", "--name", sessionName];
|
|
322
322
|
|
|
@@ -333,7 +333,7 @@ export class SpawnAcpClient implements AcpClient {
|
|
|
333
333
|
cwd: this.cwd,
|
|
334
334
|
model: this.model,
|
|
335
335
|
timeoutSeconds: this.timeoutSeconds,
|
|
336
|
-
permissionMode
|
|
336
|
+
permissionMode,
|
|
337
337
|
env: this.env,
|
|
338
338
|
pidRegistry: this.pidRegistry,
|
|
339
339
|
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Standalone completion endpoint for simple prompts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { resolvePermissions } from "../config/permissions";
|
|
7
8
|
import type { CompleteOptions } from "./types";
|
|
8
9
|
import { CompleteError } from "./types";
|
|
9
10
|
|
|
@@ -51,6 +52,11 @@ export async function executeComplete(binary: string, prompt: string, options?:
|
|
|
51
52
|
cmd.push("--output-format", "json");
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
const { skipPermissions } = resolvePermissions(options?.config, "complete");
|
|
56
|
+
if (skipPermissions) {
|
|
57
|
+
cmd.push("--dangerously-skip-permissions");
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
const spawnOpts: { stdout: "pipe"; stderr: "pipe"; cwd?: string } = { stdout: "pipe", stderr: "pipe" };
|
|
55
61
|
if (options?.workdir) spawnOpts.cwd = options.workdir;
|
|
56
62
|
const proc = _completeDeps.spawn(cmd, spawnOpts);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Handles building commands, preparing environment, and process execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { resolvePermissions } from "../config/permissions";
|
|
7
8
|
import type { PidRegistry } from "../execution/pid-registry";
|
|
8
9
|
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
9
10
|
import { getLogger } from "../logger";
|
|
@@ -49,7 +50,7 @@ export const _runOnceDeps = {
|
|
|
49
50
|
*/
|
|
50
51
|
export function buildCommand(binary: string, options: AgentRunOptions): string[] {
|
|
51
52
|
const model = options.modelDef.model;
|
|
52
|
-
const skipPermissions = options.
|
|
53
|
+
const { skipPermissions } = resolvePermissions(options.config, options.pipelineStage ?? "run");
|
|
53
54
|
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
54
55
|
return [binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
55
56
|
}
|
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
* Extracted from claude.ts: plan(), buildPlanCommand()
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { resolvePermissions } from "../config/permissions";
|
|
10
11
|
import type { PidRegistry } from "../execution/pid-registry";
|
|
11
12
|
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
12
13
|
import { getLogger } from "../logger";
|
|
@@ -30,8 +31,11 @@ export function buildPlanCommand(binary: string, options: PlanOptions): string[]
|
|
|
30
31
|
cmd.push("--model", modelDef.model);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
-
|
|
34
|
+
// Resolve permission mode from config
|
|
35
|
+
const { skipPermissions } = resolvePermissions(options.config as import("../config").NaxConfig | undefined, "plan");
|
|
36
|
+
if (skipPermissions) {
|
|
37
|
+
cmd.push("--dangerously-skip-permissions");
|
|
38
|
+
}
|
|
35
39
|
|
|
36
40
|
// Add prompt with codebase context and input file if available
|
|
37
41
|
let fullPrompt = options.prompt;
|
package/src/agents/claude.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Main adapter class coordinating execution, completion, decomposition, and interactive modes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { resolvePermissions } from "../config/permissions";
|
|
7
8
|
import { PidRegistry } from "../execution/pid-registry";
|
|
8
9
|
import { withProcessTimeout } from "../execution/timeout-handler";
|
|
9
10
|
import { getLogger } from "../logger";
|
|
@@ -185,7 +186,11 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
185
186
|
modelDef = resolveBalancedModelDef(options.config);
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
const
|
|
189
|
+
const { skipPermissions } = resolvePermissions(options.config as import("../config").NaxConfig | undefined, "run");
|
|
190
|
+
const cmd = [this.binary, "--model", modelDef.model, "-p", prompt];
|
|
191
|
+
if (skipPermissions) {
|
|
192
|
+
cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
|
|
193
|
+
}
|
|
189
194
|
|
|
190
195
|
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
191
196
|
|
package/src/agents/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* collect results from them uniformly.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { NaxConfig } from "../config";
|
|
9
10
|
import type { ModelDef, ModelTier } from "../config/schema";
|
|
10
11
|
|
|
11
12
|
// Re-export extended types for backward compatibility
|
|
@@ -76,6 +77,10 @@ export interface AgentRunOptions {
|
|
|
76
77
|
sessionRole?: string;
|
|
77
78
|
/** Max turns in multi-turn interaction loop when interactionBridge is active (default: 10) */
|
|
78
79
|
maxInteractionTurns?: number;
|
|
80
|
+
/** Pipeline stage this run belongs to — used by resolvePermissions() (default: "run") */
|
|
81
|
+
pipelineStage?: import("../config/permissions").PipelineStage;
|
|
82
|
+
/** Full nax config — passed through so adapters can call resolvePermissions() */
|
|
83
|
+
config?: NaxConfig;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/**
|
|
@@ -114,6 +119,11 @@ export interface CompleteOptions {
|
|
|
114
119
|
* Callers may also wrap complete() in their own Promise.race for shorter timeouts.
|
|
115
120
|
*/
|
|
116
121
|
timeoutMs?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Full nax config — used by resolvePermissions() to determine permission mode.
|
|
124
|
+
* Pass when available so complete() honours permissionProfile / dangerouslySkipPermissions.
|
|
125
|
+
*/
|
|
126
|
+
config?: NaxConfig;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/**
|
package/src/cli/analyze.ts
CHANGED
|
@@ -229,7 +229,7 @@ async function runDecomposeDefault(
|
|
|
229
229
|
}
|
|
230
230
|
const adapter = {
|
|
231
231
|
async decompose(prompt: string): Promise<string> {
|
|
232
|
-
return agent.complete(prompt, { jsonMode: true });
|
|
232
|
+
return agent.complete(prompt, { jsonMode: true, config });
|
|
233
233
|
},
|
|
234
234
|
};
|
|
235
235
|
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
package/src/cli/plan.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { AgentAdapter } from "../agents/types";
|
|
|
15
15
|
import { scanCodebase } from "../analyze/scanner";
|
|
16
16
|
import type { CodebaseScan } from "../analyze/types";
|
|
17
17
|
import type { NaxConfig } from "../config";
|
|
18
|
+
import { resolvePermissions } from "../config/permissions";
|
|
18
19
|
import { PidRegistry } from "../execution/pid-registry";
|
|
19
20
|
import { getLogger } from "../logger";
|
|
20
21
|
import { validatePlanOutput } from "../prd/schema";
|
|
@@ -108,7 +109,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
108
109
|
const prompt = buildPlanningPrompt(specContent, codebaseContext);
|
|
109
110
|
const cliAdapter = _deps.getAgent(agentName);
|
|
110
111
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
111
|
-
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir });
|
|
112
|
+
rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir, config });
|
|
112
113
|
// CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
|
|
113
114
|
try {
|
|
114
115
|
const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
|
|
@@ -125,13 +126,12 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
125
126
|
if (!adapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
126
127
|
const interactionBridge = createCliInteractionBridge();
|
|
127
128
|
const pidRegistry = new PidRegistry(workdir);
|
|
128
|
-
const
|
|
129
|
-
const permissionMode = dangerouslySkipPermissions ? "approve-all" : "approve-reads";
|
|
129
|
+
const resolvedPerm = resolvePermissions(config, "plan");
|
|
130
130
|
const resolvedModel = config?.plan?.model ?? "balanced";
|
|
131
131
|
logger?.info("plan", "Starting interactive planning session", {
|
|
132
132
|
agent: agentName,
|
|
133
133
|
model: resolvedModel,
|
|
134
|
-
permission:
|
|
134
|
+
permission: resolvedPerm.mode,
|
|
135
135
|
workdir,
|
|
136
136
|
feature: options.feature,
|
|
137
137
|
timeoutSeconds,
|
|
@@ -146,7 +146,7 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
146
146
|
interactionBridge,
|
|
147
147
|
config,
|
|
148
148
|
modelTier: resolvedModel,
|
|
149
|
-
dangerouslySkipPermissions,
|
|
149
|
+
dangerouslySkipPermissions: resolvedPerm.skipPermissions,
|
|
150
150
|
maxInteractionTurns: config?.agent?.maxInteractionTurns,
|
|
151
151
|
featureName: options.feature,
|
|
152
152
|
pidRegistry,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Resolver — Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* All adapters call resolvePermissions() to determine permission mode.
|
|
5
|
+
* No local fallbacks allowed elsewhere in the codebase.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1: permissionProfile field + legacy boolean backward compat.
|
|
8
|
+
* Phase 2: per-stage scoped allowlists (stub below).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { NaxConfig } from "./schema";
|
|
12
|
+
|
|
13
|
+
export type PermissionProfile = "unrestricted" | "safe" | "scoped";
|
|
14
|
+
|
|
15
|
+
export type PipelineStage =
|
|
16
|
+
| "plan"
|
|
17
|
+
| "run"
|
|
18
|
+
| "verify"
|
|
19
|
+
| "review"
|
|
20
|
+
| "rectification"
|
|
21
|
+
| "regression"
|
|
22
|
+
| "acceptance"
|
|
23
|
+
| "complete";
|
|
24
|
+
|
|
25
|
+
export interface ResolvedPermissions {
|
|
26
|
+
/** ACP permission mode string */
|
|
27
|
+
mode: "approve-all" | "approve-reads" | "default";
|
|
28
|
+
/** CLI adapter: whether to pass --dangerously-skip-permissions */
|
|
29
|
+
skipPermissions: boolean;
|
|
30
|
+
/** Future: scoped tool allowlist (Phase 2) */
|
|
31
|
+
allowedTools?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve permissions for a given pipeline stage.
|
|
36
|
+
* Single source of truth — all adapters call this.
|
|
37
|
+
*
|
|
38
|
+
* Precedence: permissionProfile > dangerouslySkipPermissions boolean > safe default.
|
|
39
|
+
*/
|
|
40
|
+
export function resolvePermissions(config: NaxConfig | undefined, _stage: PipelineStage): ResolvedPermissions {
|
|
41
|
+
const profile: PermissionProfile =
|
|
42
|
+
config?.execution?.permissionProfile ?? (config?.execution?.dangerouslySkipPermissions ? "unrestricted" : "safe");
|
|
43
|
+
|
|
44
|
+
switch (profile) {
|
|
45
|
+
case "unrestricted":
|
|
46
|
+
return { mode: "approve-all", skipPermissions: true };
|
|
47
|
+
case "safe":
|
|
48
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
49
|
+
case "scoped":
|
|
50
|
+
return resolveScopedPermissions(config, _stage);
|
|
51
|
+
default:
|
|
52
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Phase 2 stub — resolves per-stage permissions from config block.
|
|
58
|
+
* Returns safe defaults until Phase 2 is implemented.
|
|
59
|
+
*/
|
|
60
|
+
function resolveScopedPermissions(_config: NaxConfig | undefined, _stage: PipelineStage): ResolvedPermissions {
|
|
61
|
+
// Phase 2 implementation goes here
|
|
62
|
+
return { mode: "approve-reads", skipPermissions: false };
|
|
63
|
+
}
|
|
@@ -97,6 +97,17 @@ export interface ExecutionConfig {
|
|
|
97
97
|
typecheckCommand?: string | null;
|
|
98
98
|
/** Use --dangerously-skip-permissions flag for agent (default: true for backward compat, SEC-1 fix) */
|
|
99
99
|
dangerouslySkipPermissions?: boolean;
|
|
100
|
+
/** Permission profile — takes precedence over dangerouslySkipPermissions (Phase 1) */
|
|
101
|
+
permissionProfile?: "unrestricted" | "safe" | "scoped";
|
|
102
|
+
/** Per-stage permission overrides — only read when permissionProfile = "scoped" (Phase 2) */
|
|
103
|
+
permissions?: Record<
|
|
104
|
+
string,
|
|
105
|
+
{
|
|
106
|
+
mode: "approve-all" | "approve-reads" | "scoped";
|
|
107
|
+
allowedTools?: string[];
|
|
108
|
+
inherit?: string;
|
|
109
|
+
}
|
|
110
|
+
>;
|
|
100
111
|
/** Enable smart test runner to scope test runs to changed files (default: true).
|
|
101
112
|
* Accepts boolean for backward compat or a SmartTestRunnerConfig object. */
|
|
102
113
|
smartTestRunner?: boolean | SmartTestRunnerConfig;
|
package/src/config/schemas.ts
CHANGED
|
@@ -105,7 +105,21 @@ const ExecutionConfigSchema = z.object({
|
|
|
105
105
|
.default(2000),
|
|
106
106
|
lintCommand: z.string().nullable().optional(),
|
|
107
107
|
typecheckCommand: z.string().nullable().optional(),
|
|
108
|
+
// DEPRECATED — use permissionProfile instead. Kept for backward compat.
|
|
108
109
|
dangerouslySkipPermissions: z.boolean().default(true),
|
|
110
|
+
// NEW — takes precedence over dangerouslySkipPermissions
|
|
111
|
+
permissionProfile: z.enum(["unrestricted", "safe", "scoped"]).optional(),
|
|
112
|
+
// Phase 2: per-stage permission overrides (only read when profile = "scoped")
|
|
113
|
+
permissions: z
|
|
114
|
+
.record(
|
|
115
|
+
z.string(),
|
|
116
|
+
z.object({
|
|
117
|
+
mode: z.enum(["approve-all", "approve-reads", "scoped"]),
|
|
118
|
+
allowedTools: z.array(z.string()).optional(),
|
|
119
|
+
inherit: z.string().optional(),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
.optional(),
|
|
109
123
|
smartTestRunner: smartTestRunnerFieldSchema,
|
|
110
124
|
});
|
|
111
125
|
|
|
@@ -156,6 +156,7 @@ export class AutoInteractionPlugin implements InteractionPlugin {
|
|
|
156
156
|
const output = await adapter.complete(prompt, {
|
|
157
157
|
...(modelArg && { model: modelArg }),
|
|
158
158
|
jsonMode: true,
|
|
159
|
+
...(this.config.naxConfig && { config: this.config.naxConfig }),
|
|
159
160
|
});
|
|
160
161
|
|
|
161
162
|
return this.parseResponse(output);
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { getAgent, validateAgentForTier } from "../../agents";
|
|
34
34
|
import { resolveModel } from "../../config";
|
|
35
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
35
36
|
import { checkMergeConflict, checkStoryAmbiguity, isTriggerEnabled } from "../../interaction/triggers";
|
|
36
37
|
import { getLogger } from "../../logger";
|
|
37
38
|
import type { FailureCategory } from "../../tdd";
|
|
@@ -217,7 +218,9 @@ export const executionStage: PipelineStage = {
|
|
|
217
218
|
modelTier: ctx.routing.modelTier,
|
|
218
219
|
modelDef: resolveModel(ctx.config.models[ctx.routing.modelTier]),
|
|
219
220
|
timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
|
|
220
|
-
dangerouslySkipPermissions: ctx.config.
|
|
221
|
+
dangerouslySkipPermissions: resolvePermissions(ctx.config, "run").skipPermissions,
|
|
222
|
+
pipelineStage: "run",
|
|
223
|
+
config: ctx.config,
|
|
221
224
|
maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
|
|
222
225
|
pidRegistry: ctx.pidRegistry,
|
|
223
226
|
featureName: ctx.prd.feature,
|
|
@@ -111,7 +111,7 @@ async function callLlmOnce(
|
|
|
111
111
|
// Prevent unhandled rejection if timer fires between race resolution and clearTimeout
|
|
112
112
|
timeoutPromise.catch(() => {});
|
|
113
113
|
|
|
114
|
-
const outputPromise = adapter.complete(prompt, { model: modelArg });
|
|
114
|
+
const outputPromise = adapter.complete(prompt, { model: modelArg, config });
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
117
|
const result = await Promise.race([outputPromise, timeoutPromise]);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { AgentAdapter } from "../agents";
|
|
10
10
|
import type { ModelTier, NaxConfig } from "../config";
|
|
11
11
|
import { resolveModel } from "../config";
|
|
12
|
+
import { resolvePermissions } from "../config/permissions";
|
|
12
13
|
import type { getLogger } from "../logger";
|
|
13
14
|
import type { UserStory } from "../prd";
|
|
14
15
|
import { autoCommitIfDirty, captureGitRef } from "../utils/git";
|
|
@@ -158,7 +159,9 @@ async function runRectificationLoop(
|
|
|
158
159
|
modelTier: implementerTier,
|
|
159
160
|
modelDef: resolveModel(config.models[implementerTier]),
|
|
160
161
|
timeoutSeconds: config.execution.sessionTimeoutSeconds,
|
|
161
|
-
dangerouslySkipPermissions: config.
|
|
162
|
+
dangerouslySkipPermissions: resolvePermissions(config, "rectification").skipPermissions,
|
|
163
|
+
pipelineStage: "rectification",
|
|
164
|
+
config,
|
|
162
165
|
maxInteractionTurns: config.agent?.maxInteractionTurns,
|
|
163
166
|
featureName,
|
|
164
167
|
storyId: story.id,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { AgentAdapter } from "../agents";
|
|
8
8
|
import type { ModelTier, NaxConfig } from "../config";
|
|
9
9
|
import { resolveModel } from "../config";
|
|
10
|
+
import { resolvePermissions } from "../config/permissions";
|
|
10
11
|
import { getLogger } from "../logger";
|
|
11
12
|
import type { UserStory } from "../prd";
|
|
12
13
|
import { PromptBuilder } from "../prompts";
|
|
@@ -139,7 +140,9 @@ export async function runTddSession(
|
|
|
139
140
|
modelTier,
|
|
140
141
|
modelDef: resolveModel(config.models[modelTier]),
|
|
141
142
|
timeoutSeconds: config.execution.sessionTimeoutSeconds,
|
|
142
|
-
dangerouslySkipPermissions: config.
|
|
143
|
+
dangerouslySkipPermissions: resolvePermissions(config, "run").skipPermissions,
|
|
144
|
+
pipelineStage: "run",
|
|
145
|
+
config,
|
|
143
146
|
maxInteractionTurns: config.agent?.maxInteractionTurns,
|
|
144
147
|
featureName,
|
|
145
148
|
storyId: story.id,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { getAgent } from "../agents";
|
|
11
11
|
import type { NaxConfig } from "../config";
|
|
12
12
|
import { resolveModel } from "../config";
|
|
13
|
+
import { resolvePermissions } from "../config/permissions";
|
|
13
14
|
import { parseBunTestOutput } from "../execution/test-output-parser";
|
|
14
15
|
import { getSafeLogger } from "../logger";
|
|
15
16
|
import type { UserStory } from "../prd";
|
|
@@ -73,7 +74,9 @@ export async function runRectificationLoop(opts: RectificationLoopOptions): Prom
|
|
|
73
74
|
modelTier,
|
|
74
75
|
modelDef,
|
|
75
76
|
timeoutSeconds: config.execution.sessionTimeoutSeconds,
|
|
76
|
-
dangerouslySkipPermissions: config.
|
|
77
|
+
dangerouslySkipPermissions: resolvePermissions(config, "rectification").skipPermissions,
|
|
78
|
+
pipelineStage: "rectification",
|
|
79
|
+
config,
|
|
77
80
|
maxInteractionTurns: config.agent?.maxInteractionTurns,
|
|
78
81
|
});
|
|
79
82
|
|