agentic-orchestrator 0.1.7 → 0.1.9
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/README.md +25 -3
- package/agentic/orchestrator/schemas/agents.schema.json +1 -1
- package/apps/control-plane/src/cli/dashboard-command-handler.ts +42 -5
- package/apps/control-plane/src/cli/env-file.ts +115 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +1 -1
- package/apps/control-plane/src/cli/init-command-handler.ts +72 -2
- package/apps/control-plane/src/cli/retry-command-handler.ts +0 -1
- package/apps/control-plane/src/core/kernel.ts +1 -3
- package/apps/control-plane/src/core/tool-caller.ts +18 -3
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +10 -3
- package/apps/control-plane/src/providers/providers.ts +67 -8
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +21 -4
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +21 -4
- package/apps/control-plane/src/supervisor/runtime.ts +9 -4
- package/apps/control-plane/src/supervisor/types.ts +1 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +4 -0
- package/apps/control-plane/test/dashboard-command.spec.ts +36 -0
- package/apps/control-plane/test/init-wizard.spec.ts +166 -1
- package/apps/control-plane/test/providers.spec.ts +75 -2
- package/apps/control-plane/test/supervisor-collaborators.spec.ts +86 -0
- package/apps/control-plane/test/supervisor.unit.spec.ts +1 -1
- package/config/agentic/orchestrator/adapters.yaml +3 -0
- package/config/agentic/orchestrator/agents.yaml +13 -0
- package/config/agentic/orchestrator/gates.yaml +28 -0
- package/config/agentic/orchestrator/policy.yaml +22 -0
- package/config/agentic/orchestrator/prompts/builder.system.md +1 -0
- package/config/agentic/orchestrator/prompts/planner.system.md +16 -0
- package/config/agentic/orchestrator/prompts/qa.system.md +1 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js +32 -5
- package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/env-file.d.ts +4 -0
- package/dist/apps/control-plane/cli/env-file.js +89 -0
- package/dist/apps/control-plane/cli/env-file.js.map +1 -0
- package/dist/apps/control-plane/cli/help-command-handler.js +1 -1
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/init-command-handler.js +53 -4
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/retry-command-handler.js +0 -1
- package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +11 -1
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +9 -3
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +2 -1
- package/dist/apps/control-plane/providers/providers.js +52 -7
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +20 -4
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +20 -4
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +2 -2
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +1 -1
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/package.json +1 -1
- package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +2 -0
- package/spec-files/outstanding/agentic_orchestrator_provider_auth_bootstrap_spec.md +384 -0
- package/spec-files/progress.md +19 -0
package/README.md
CHANGED
|
@@ -247,6 +247,12 @@ Behavior:
|
|
|
247
247
|
1. CLI flags (`--agent-provider`, `--agent-model`, `--agent-config`, `--provider-config-env`)
|
|
248
248
|
2. env vars (`AOP_AGENT_PROVIDER`, `AOP_AGENT_MODEL`, `AOP_AGENT_CONFIG`/`AOP_AGENT_CONFIG_JSON`, `AOP_PROVIDER_CONFIG_ENV`)
|
|
249
249
|
3. `config/agentic/orchestrator/agents.yaml` runtime defaults
|
|
250
|
+
- Provider credential resolution (`provider_config_ref`):
|
|
251
|
+
1. `--provider-config-env <NAME>` if `NAME` exists in env
|
|
252
|
+
2. `agents.yaml runtime.provider_config_env` if that env var exists
|
|
253
|
+
3. `AOP_PROVIDER_CONFIG_ENV` fallback:
|
|
254
|
+
- if it looks like an env-var name and that env var exists, use that value (legacy indirection)
|
|
255
|
+
- otherwise use `AOP_PROVIDER_CONFIG_ENV` as the direct credential value
|
|
250
256
|
- Runtime start:
|
|
251
257
|
- starts `SupervisorRuntime` with `max_active_features=5`, `max_parallel_gate_runs=3`.
|
|
252
258
|
- `max_iterations_per_phase` resolves from `policy.yaml` (`supervisor.max_iterations_per_phase`, default `6`).
|
|
@@ -376,7 +382,7 @@ When `cleanup.auto_after_merge` is enabled in `policy.yaml`, the runtime automat
|
|
|
376
382
|
|
|
377
383
|
### `init`
|
|
378
384
|
|
|
379
|
-
Initialises agentic orchestrator configuration in the current directory. Generates `policy.yaml`, `gates.yaml`, `agents.yaml`, `adapters.yaml`, and system prompt templates. The wizard also captures default agent `provider`/`model` and
|
|
385
|
+
Initialises agentic orchestrator configuration in the current directory. Generates `policy.yaml`, `gates.yaml`, `agents.yaml`, `adapters.yaml`, and system prompt templates. The wizard also captures default agent `provider`/`model`, `scm-provider`, and provider-auth mode.
|
|
380
386
|
|
|
381
387
|
Schema files are bundled with AOP and validated from the installation path; `aop init` does not copy schemas into the target repository.
|
|
382
388
|
|
|
@@ -414,6 +420,14 @@ At runtime the kernel always composes a canonical full policy by:
|
|
|
414
420
|
|
|
415
421
|
Existing repositories with full `policy.yaml` files continue to work without changes — user values override defaults entirely.
|
|
416
422
|
|
|
423
|
+
#### Provider Auth Flow In `aop init`
|
|
424
|
+
|
|
425
|
+
- wizard prompt: `Will you use a local agent CLI ... (yes/no)` (default `yes`)
|
|
426
|
+
- if `yes`: init skips `runtime.provider_config_env` in `agents.yaml`
|
|
427
|
+
- if `no`: init asks for provider env var name and checks both process env and repo `.env`
|
|
428
|
+
- if missing: init prompts for a key, stores it in `.env` as `AOP_PROVIDER_CONFIG_ENV`, and writes `runtime.provider_config_env: AOP_PROVIDER_CONFIG_ENV`
|
|
429
|
+
- `aop init --auto` defaults to local-CLI mode and does not emit `provider_config_env`
|
|
430
|
+
|
|
417
431
|
### `dashboard`
|
|
418
432
|
|
|
419
433
|
Starts the web dashboard server (`packages/web-dashboard/`). Provides a real-time Kanban view of all features, review approval/denial, checkout, and SSE-based live updates.
|
|
@@ -484,7 +498,7 @@ Supported options:
|
|
|
484
498
|
| `--agent-provider <codex\\ | claude\\ | gemini\\ | custom\\ | kiro-cli\\ | copilot>` | Provider selection |
|
|
485
499
|
| `--agent-model <model-id>` | Model selection |
|
|
486
500
|
| `--agent-config <json-object>` | Additional provider-specific agent config (for example command/args payload) |
|
|
487
|
-
| `--provider-config-env <ENV_VAR>` | Provider auth/config
|
|
501
|
+
| `--provider-config-env <ENV_VAR>` | Provider auth/config env var name for API-backed providers |
|
|
488
502
|
| `--transport <mcp\\ | inprocess>` | Tool transport selection (default `mcp`) |
|
|
489
503
|
| `--takeover-stale-run` | Allow stale run-lease takeover during `run` |
|
|
490
504
|
| `--project <name>` | Select project from `multi-project.yaml` (run, status, resume, retry) |
|
|
@@ -525,6 +539,14 @@ Provider resolution precedence:
|
|
|
525
539
|
2. env vars (`AOP_AGENT_PROVIDER`, `AOP_AGENT_MODEL`, `AOP_AGENT_CONFIG`/`AOP_AGENT_CONFIG_JSON`, `AOP_PROVIDER_CONFIG_ENV`)
|
|
526
540
|
3. `config/agentic/orchestrator/agents.yaml` runtime defaults
|
|
527
541
|
|
|
542
|
+
Provider credential fallback:
|
|
543
|
+
|
|
544
|
+
1. `--provider-config-env <NAME>` if `NAME` exists
|
|
545
|
+
2. `runtime.provider_config_env` if that env var exists
|
|
546
|
+
3. `AOP_PROVIDER_CONFIG_ENV`:
|
|
547
|
+
- `AOP_PROVIDER_CONFIG_ENV=OTHER_ENV` and `OTHER_ENV` exists -> use `OTHER_ENV`
|
|
548
|
+
- otherwise treat `AOP_PROVIDER_CONFIG_ENV` as direct credential value
|
|
549
|
+
|
|
528
550
|
Transport behavior:
|
|
529
551
|
|
|
530
552
|
- default transport is `mcp`
|
|
@@ -571,7 +593,7 @@ Coverage parser:
|
|
|
571
593
|
### Agents (`config/agentic/orchestrator/agents.yaml`)
|
|
572
594
|
|
|
573
595
|
- role-specific system prompt paths
|
|
574
|
-
- default provider/model
|
|
596
|
+
- default provider/model values with optional `runtime.provider_config_env` for API-backed providers
|
|
575
597
|
- optional `runtime.provider_configs.<provider>` objects for provider-specific payloads (for example `kiro-cli chat --agent dev`)
|
|
576
598
|
- `worktree.post_create` commands and `worktree.symlinks` for workspace hook automation
|
|
577
599
|
- stack-specific examples: [`example-configurations/node/`](example-configurations/node) and [`example-configurations/java/`](example-configurations/java)
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"provider_config_env": {
|
|
56
56
|
"type": "string",
|
|
57
|
-
"description": "
|
|
57
|
+
"description": "Optional env var name for API-backed provider credentials/config. Local CLI providers typically omit this. Runtime also supports AOP_PROVIDER_CONFIG_ENV fallback (legacy indirection or direct credential value)."
|
|
58
58
|
},
|
|
59
59
|
"provider_configs": {
|
|
60
60
|
"type": "object",
|
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
const DASHBOARD_WORKSPACE = '@aop/web-dashboard';
|
|
10
|
+
|
|
11
|
+
async function hasDashboardCli(repoRoot: string): Promise<boolean> {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(repoRoot, 'node_modules', '.bin', 'next'),
|
|
14
|
+
path.join(repoRoot, 'packages', 'web-dashboard', 'node_modules', '.bin', 'next'),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(candidate);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
// continue
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function ensureDashboardDependencies(
|
|
28
|
+
repoRoot: string,
|
|
29
|
+
env: NodeJS.ProcessEnv,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
if (await hasDashboardCli(repoRoot)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await execFileAsync(
|
|
36
|
+
'npm',
|
|
37
|
+
['install', '--workspace', DASHBOARD_WORKSPACE, '--no-audit', '--no-fund'],
|
|
38
|
+
{
|
|
39
|
+
cwd: repoRoot,
|
|
40
|
+
env,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
}
|
|
8
44
|
|
|
9
45
|
export class DashboardCommandHandler {
|
|
10
46
|
async execute(options: {
|
|
@@ -14,7 +50,6 @@ export class DashboardCommandHandler {
|
|
|
14
50
|
}): Promise<Record<string, unknown>> {
|
|
15
51
|
const port = options.port ?? 3000;
|
|
16
52
|
const repoRoot = path.resolve(__dirname, '../../../../');
|
|
17
|
-
const dashboardWorkspace = '@aop/web-dashboard';
|
|
18
53
|
const devMode = options.dev === true;
|
|
19
54
|
const foreground = options.foreground === true || devMode;
|
|
20
55
|
|
|
@@ -25,8 +60,10 @@ export class DashboardCommandHandler {
|
|
|
25
60
|
AOP_ROOT: process.cwd(),
|
|
26
61
|
};
|
|
27
62
|
|
|
63
|
+
await ensureDashboardDependencies(repoRoot, env);
|
|
64
|
+
|
|
28
65
|
if (devMode) {
|
|
29
|
-
const child = spawn('npm', ['run', '--workspace',
|
|
66
|
+
const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'dev'], {
|
|
30
67
|
cwd: repoRoot,
|
|
31
68
|
env,
|
|
32
69
|
stdio: 'inherit',
|
|
@@ -38,13 +75,13 @@ export class DashboardCommandHandler {
|
|
|
38
75
|
return { ok: true, data: { message: 'Dashboard stopped', port, mode: 'dev' } };
|
|
39
76
|
}
|
|
40
77
|
|
|
41
|
-
await execFileAsync('npm', ['run', '--workspace',
|
|
78
|
+
await execFileAsync('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'build'], {
|
|
42
79
|
cwd: repoRoot,
|
|
43
80
|
env,
|
|
44
81
|
});
|
|
45
82
|
|
|
46
83
|
if (foreground) {
|
|
47
|
-
const child = spawn('npm', ['run', '--workspace',
|
|
84
|
+
const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'start'], {
|
|
48
85
|
cwd: repoRoot,
|
|
49
86
|
env,
|
|
50
87
|
stdio: 'inherit',
|
|
@@ -56,7 +93,7 @@ export class DashboardCommandHandler {
|
|
|
56
93
|
return { ok: true, data: { message: 'Dashboard stopped', port, mode: 'production' } };
|
|
57
94
|
}
|
|
58
95
|
|
|
59
|
-
const child = spawn('npm', ['run', '--workspace',
|
|
96
|
+
const child = spawn('npm', ['run', '--workspace', DASHBOARD_WORKSPACE, 'start'], {
|
|
60
97
|
cwd: repoRoot,
|
|
61
98
|
env,
|
|
62
99
|
detached: true,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export type EnvFileValues = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
const SAFE_UNQUOTED_ENV_VALUE = /^[A-Za-z0-9_./:@+=-]+$/;
|
|
6
|
+
|
|
7
|
+
function unquoteValue(raw: string): string {
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (
|
|
10
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
11
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
12
|
+
) {
|
|
13
|
+
return trimmed.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseEnvLine(line: string): { key: string; value: string } | null {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalized = trimmed.startsWith('export ') ? trimmed.slice('export '.length) : trimmed;
|
|
25
|
+
const separatorIndex = normalized.indexOf('=');
|
|
26
|
+
if (separatorIndex <= 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
31
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const value = normalized.slice(separatorIndex + 1);
|
|
36
|
+
return { key, value: unquoteValue(value) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatEnvValue(value: string): string {
|
|
40
|
+
if (SAFE_UNQUOTED_ENV_VALUE.test(value)) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readEnvFileValues(envPath: string): Promise<EnvFileValues> {
|
|
47
|
+
let content: string;
|
|
48
|
+
try {
|
|
49
|
+
content = await fs.readFile(envPath, 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const values: EnvFileValues = {};
|
|
55
|
+
for (const line of content.split(/\r?\n/)) {
|
|
56
|
+
const parsed = parseEnvLine(line);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
values[parsed.key] = parsed.value;
|
|
61
|
+
}
|
|
62
|
+
return values;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readNonEmptyEnvValue(
|
|
66
|
+
key: string,
|
|
67
|
+
runtimeEnv: NodeJS.ProcessEnv,
|
|
68
|
+
envFileValues: EnvFileValues,
|
|
69
|
+
): string | null {
|
|
70
|
+
const runtimeValue = runtimeEnv[key];
|
|
71
|
+
if (typeof runtimeValue === 'string' && runtimeValue.trim().length > 0) {
|
|
72
|
+
return runtimeValue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fileValue = envFileValues[key];
|
|
76
|
+
if (typeof fileValue === 'string' && fileValue.trim().length > 0) {
|
|
77
|
+
return fileValue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function upsertEnvFileValue(
|
|
84
|
+
envPath: string,
|
|
85
|
+
key: string,
|
|
86
|
+
value: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
let content = '';
|
|
89
|
+
try {
|
|
90
|
+
content = await fs.readFile(envPath, 'utf8');
|
|
91
|
+
} catch {
|
|
92
|
+
// create file from scratch
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = content.length > 0 ? content.split(/\r?\n/) : [];
|
|
96
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
const matcher = new RegExp(`^\\s*(?:export\\s+)?${escapedKey}\\s*=`);
|
|
98
|
+
const nextLine = `${key}=${formatEnvValue(value)}`;
|
|
99
|
+
|
|
100
|
+
let replaced = false;
|
|
101
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
102
|
+
if (matcher.test(lines[index])) {
|
|
103
|
+
lines[index] = nextLine;
|
|
104
|
+
replaced = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!replaced) {
|
|
110
|
+
lines.push(nextLine);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalized = `${lines.join('\n').replace(/\n*$/, '')}\n`;
|
|
114
|
+
await fs.writeFile(envPath, normalized, 'utf8');
|
|
115
|
+
}
|
|
@@ -25,7 +25,7 @@ const COMMAND_HELP: Record<CliCommand, CommandHelp> = {
|
|
|
25
25
|
{ flag: '--agent-config <PATH>', description: 'Path to agent config file' },
|
|
26
26
|
{
|
|
27
27
|
flag: '--provider-config-env <var>',
|
|
28
|
-
description: 'Env var name
|
|
28
|
+
description: 'Env var name used for API-backed provider auth/config',
|
|
29
29
|
},
|
|
30
30
|
{ flag: '--transport <inprocess|mcp>', description: 'Tool transport layer (default: mcp)' },
|
|
31
31
|
{ flag: '--takeover-stale-run', description: 'Take over a stale run lease' },
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import YAML from 'yaml';
|
|
9
9
|
import { SchemaRegistry } from '../core/schemas.js';
|
|
10
10
|
import { loadComposedPolicy } from '../application/services/policy-loader-service.js';
|
|
11
|
+
import { readEnvFileValues, readNonEmptyEnvValue, upsertEnvFileValue } from './env-file.js';
|
|
11
12
|
import {
|
|
12
13
|
AGENT_PROVIDER_SLOT,
|
|
13
14
|
SCM_PROVIDER_SLOT,
|
|
@@ -47,6 +48,8 @@ interface WizardConfig {
|
|
|
47
48
|
framework: TestFramework;
|
|
48
49
|
defaultProvider: string;
|
|
49
50
|
defaultModel: string;
|
|
51
|
+
providerConfigEnv: string | null;
|
|
52
|
+
providerCredentialBootstrapped: boolean;
|
|
50
53
|
scmProvider: string;
|
|
51
54
|
notifications: {
|
|
52
55
|
desktop: boolean;
|
|
@@ -402,6 +405,9 @@ capabilities:
|
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
function generateAgentsYaml(wizard: WizardConfig): string {
|
|
408
|
+
const providerConfigEnvLine = wizard.providerConfigEnv
|
|
409
|
+
? ` provider_config_env: ${wizard.providerConfigEnv}\n`
|
|
410
|
+
: '';
|
|
405
411
|
return `version: 1
|
|
406
412
|
roles:
|
|
407
413
|
planner:
|
|
@@ -414,8 +420,7 @@ missing_prompt_behavior: ignore
|
|
|
414
420
|
runtime:
|
|
415
421
|
default_provider: ${wizard.defaultProvider}
|
|
416
422
|
default_model: ${wizard.defaultModel}
|
|
417
|
-
|
|
418
|
-
role_provider_overrides: {}
|
|
423
|
+
${providerConfigEnvLine} role_provider_overrides: {}
|
|
419
424
|
`;
|
|
420
425
|
}
|
|
421
426
|
|
|
@@ -512,7 +517,26 @@ async function askWithDefault(
|
|
|
512
517
|
return raw.length > 0 ? raw : defaultValue;
|
|
513
518
|
}
|
|
514
519
|
|
|
520
|
+
function parseYesNo(raw: string, fallback: boolean): boolean {
|
|
521
|
+
const normalized = raw.trim().toLowerCase();
|
|
522
|
+
if (normalized === 'yes' || normalized === 'y' || normalized === 'true') {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (normalized === 'no' || normalized === 'n' || normalized === 'false') {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
return fallback;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function defaultProviderConfigEnvName(provider: string): string {
|
|
532
|
+
if (provider === 'gemini') {
|
|
533
|
+
return 'GEMINI_API_KEY';
|
|
534
|
+
}
|
|
535
|
+
return 'AOP_PROVIDER_CONFIG_ENV';
|
|
536
|
+
}
|
|
537
|
+
|
|
515
538
|
async function collectWizardConfig(
|
|
539
|
+
repoRoot: string,
|
|
516
540
|
defaults: {
|
|
517
541
|
branch: string;
|
|
518
542
|
framework: TestFramework;
|
|
@@ -524,6 +548,8 @@ async function collectWizardConfig(
|
|
|
524
548
|
): Promise<WizardConfig> {
|
|
525
549
|
const prompt = promptFactory();
|
|
526
550
|
try {
|
|
551
|
+
const envFilePath = path.join(repoRoot, '.env');
|
|
552
|
+
const envFileValues = await readEnvFileValues(envFilePath);
|
|
527
553
|
const supportedProviders = new Set(
|
|
528
554
|
globalAdapterRegistry.list(AGENT_PROVIDER_SLOT.name).map((adapter) => adapter.name),
|
|
529
555
|
);
|
|
@@ -567,6 +593,42 @@ async function collectWizardConfig(
|
|
|
567
593
|
webhookUrl = (await prompt.question('Webhook URL []: ')).trim();
|
|
568
594
|
}
|
|
569
595
|
|
|
596
|
+
const localCliAnswer = await askWithDefault(
|
|
597
|
+
prompt,
|
|
598
|
+
'Will you use a local agent CLI (codex/claude-code/kiro-cli/copilot)? (yes/no)',
|
|
599
|
+
'yes',
|
|
600
|
+
);
|
|
601
|
+
const usesLocalCli = parseYesNo(localCliAnswer, true);
|
|
602
|
+
|
|
603
|
+
let providerConfigEnv: string | null = null;
|
|
604
|
+
let providerCredentialBootstrapped = false;
|
|
605
|
+
|
|
606
|
+
if (!usesLocalCli) {
|
|
607
|
+
const envVarName = await askWithDefault(
|
|
608
|
+
prompt,
|
|
609
|
+
'Provider config env var name',
|
|
610
|
+
defaultProviderConfigEnvName(defaultProviderRaw),
|
|
611
|
+
);
|
|
612
|
+
const envValue = readNonEmptyEnvValue(envVarName, process.env, envFileValues);
|
|
613
|
+
|
|
614
|
+
if (envValue) {
|
|
615
|
+
providerConfigEnv = envVarName;
|
|
616
|
+
} else {
|
|
617
|
+
output.write(
|
|
618
|
+
`Environment variable "${envVarName}" was not found in process env or ${envFilePath}.\n`,
|
|
619
|
+
);
|
|
620
|
+
let pastedKey = '';
|
|
621
|
+
while (pastedKey.length === 0) {
|
|
622
|
+
pastedKey = (
|
|
623
|
+
await prompt.question('Paste provider key to store in AOP_PROVIDER_CONFIG_ENV: ')
|
|
624
|
+
).trim();
|
|
625
|
+
}
|
|
626
|
+
await upsertEnvFileValue(envFilePath, 'AOP_PROVIDER_CONFIG_ENV', pastedKey);
|
|
627
|
+
providerConfigEnv = 'AOP_PROVIDER_CONFIG_ENV';
|
|
628
|
+
providerCredentialBootstrapped = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
570
632
|
return {
|
|
571
633
|
baseBranch,
|
|
572
634
|
defaultProvider: parseAdapterName(
|
|
@@ -575,6 +637,8 @@ async function collectWizardConfig(
|
|
|
575
637
|
defaults.defaultProvider,
|
|
576
638
|
),
|
|
577
639
|
defaultModel,
|
|
640
|
+
providerConfigEnv,
|
|
641
|
+
providerCredentialBootstrapped,
|
|
578
642
|
scmProvider: parseAdapterName(
|
|
579
643
|
scmProviderRaw,
|
|
580
644
|
supportedScmProviders,
|
|
@@ -633,6 +697,8 @@ export class InitCommandHandler {
|
|
|
633
697
|
baseBranch: gitContext.defaultBranch,
|
|
634
698
|
defaultProvider: DEFAULT_AGENT_PROVIDER,
|
|
635
699
|
defaultModel: DEFAULT_AGENT_MODEL,
|
|
700
|
+
providerConfigEnv: null,
|
|
701
|
+
providerCredentialBootstrapped: false,
|
|
636
702
|
scmProvider: DEFAULT_SCM_PROVIDER,
|
|
637
703
|
maxParallelGateRuns: 3,
|
|
638
704
|
dashboardPort: 3000,
|
|
@@ -646,6 +712,7 @@ export class InitCommandHandler {
|
|
|
646
712
|
},
|
|
647
713
|
}
|
|
648
714
|
: await collectWizardConfig(
|
|
715
|
+
this.repoRoot,
|
|
649
716
|
{
|
|
650
717
|
branch: gitContext.defaultBranch,
|
|
651
718
|
framework,
|
|
@@ -758,6 +825,9 @@ export class InitCommandHandler {
|
|
|
758
825
|
'To generate a full explicit policy with all advanced controls, re-run: aop init --advanced-policy --force',
|
|
759
826
|
);
|
|
760
827
|
}
|
|
828
|
+
if (wizard.providerCredentialBootstrapped) {
|
|
829
|
+
nextSteps.push('Stored provider credential in .env as AOP_PROVIDER_CONFIG_ENV.');
|
|
830
|
+
}
|
|
761
831
|
|
|
762
832
|
return {
|
|
763
833
|
ok: true,
|
|
@@ -1001,9 +1001,7 @@ export class AopKernel {
|
|
|
1001
1001
|
await atomicWriteJson(runLeasePath, data);
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
-
async acquireRunLease(
|
|
1005
|
-
input: AcquireRunLeaseInput,
|
|
1006
|
-
): Promise<{
|
|
1004
|
+
async acquireRunLease(input: AcquireRunLeaseInput): Promise<{
|
|
1007
1005
|
data: {
|
|
1008
1006
|
runtime_sessions: RuntimeSessionsSnapshot;
|
|
1009
1007
|
took_over_stale: boolean;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
export type RuntimeRole = 'orchestrator' | 'planner' | 'builder' | 'qa';
|
|
2
2
|
|
|
3
|
+
export interface GatesRunToolArgs {
|
|
4
|
+
feature_id: string;
|
|
5
|
+
mode: string;
|
|
6
|
+
profile?: string;
|
|
7
|
+
operation_id?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolArgsByName {
|
|
11
|
+
'gates.run': GatesRunToolArgs;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ToolArgs<TToolName extends string> = TToolName extends keyof ToolArgsByName
|
|
15
|
+
? ToolArgsByName[TToolName]
|
|
16
|
+
: Record<string, unknown>;
|
|
17
|
+
|
|
3
18
|
export interface ToolCaller {
|
|
4
|
-
callTool<TData = Record<string, unknown
|
|
19
|
+
callTool<TData = Record<string, unknown>, TToolName extends string = string>(
|
|
5
20
|
role: RuntimeRole,
|
|
6
|
-
toolName:
|
|
7
|
-
args:
|
|
21
|
+
toolName: TToolName,
|
|
22
|
+
args: ToolArgs<TToolName>,
|
|
8
23
|
): Promise<{ ok: true; data: TData }>;
|
|
9
24
|
}
|
|
10
25
|
|
|
@@ -24,6 +24,7 @@ import { RetryCommandHandler } from '../../cli/retry-command-handler.js';
|
|
|
24
24
|
import { SendCommandHandler } from '../../cli/send-command-handler.js';
|
|
25
25
|
import { AttachCommandHandler } from '../../cli/attach-command-handler.js';
|
|
26
26
|
import { HelpCommandHandler } from '../../cli/help-command-handler.js';
|
|
27
|
+
import { readEnvFileValues } from '../../cli/env-file.js';
|
|
27
28
|
import { MultiProjectLoader } from '../../application/multi-project-loader.js';
|
|
28
29
|
import { NullWorkerProvider, resolveProviderSelection } from '../../providers/providers.js';
|
|
29
30
|
import type { RuntimeContext } from '../../cli/types.js';
|
|
@@ -154,6 +155,12 @@ export async function runCli(
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
try {
|
|
158
|
+
const envFileValues = await readEnvFileValues(path.join(repoRoot, '.env'));
|
|
159
|
+
const effectiveEnv: NodeJS.ProcessEnv = {
|
|
160
|
+
...envFileValues,
|
|
161
|
+
...runtime.env,
|
|
162
|
+
};
|
|
163
|
+
|
|
157
164
|
if (!SUPPORTED_COMMANDS.has(options.command)) {
|
|
158
165
|
printError(ERROR_CODES.INVALID_CLI_ARGS, `Unknown command: ${options.command}`, {
|
|
159
166
|
command: options.command,
|
|
@@ -264,7 +271,7 @@ export async function runCli(
|
|
|
264
271
|
try {
|
|
265
272
|
selection = resolveProviderSelection({
|
|
266
273
|
cli: options as unknown as Record<string, string | undefined>,
|
|
267
|
-
env:
|
|
274
|
+
env: effectiveEnv,
|
|
268
275
|
agentsConfig: kernel.getAgentsConfig(),
|
|
269
276
|
});
|
|
270
277
|
} catch {
|
|
@@ -290,7 +297,7 @@ export async function runCli(
|
|
|
290
297
|
const handler = new ResumeCommandHandler();
|
|
291
298
|
const payload = await handler.execute({
|
|
292
299
|
repoRoot,
|
|
293
|
-
env:
|
|
300
|
+
env: effectiveEnv,
|
|
294
301
|
runId,
|
|
295
302
|
transport,
|
|
296
303
|
options,
|
|
@@ -362,7 +369,7 @@ export async function runCli(
|
|
|
362
369
|
const handler = new RunCommandHandler();
|
|
363
370
|
const payload = await handler.execute({
|
|
364
371
|
repoRoot,
|
|
365
|
-
env:
|
|
372
|
+
env: effectiveEnv,
|
|
366
373
|
runId,
|
|
367
374
|
transport,
|
|
368
375
|
options,
|
|
@@ -31,6 +31,7 @@ interface ResolveSelectionInput {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
type ResolveSelectionRuntimeConfig = NonNullable<ResolveSelectionInput['agentsConfig']>['runtime'];
|
|
34
|
+
const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
34
35
|
|
|
35
36
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
37
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -87,8 +88,66 @@ function resolveConfiguredAgentConfig(
|
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function readNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string | null): string | null {
|
|
92
|
+
if (!key) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const value = env[key];
|
|
96
|
+
if (typeof value !== 'string') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return value.trim().length > 0 ? value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveProviderCredentialReference(
|
|
103
|
+
cliProviderConfigEnv: string | undefined,
|
|
104
|
+
configProviderConfigEnv: string | undefined,
|
|
105
|
+
env: NodeJS.ProcessEnv,
|
|
106
|
+
): { providerConfigEnv: string | null; providerConfigRef: string | null } {
|
|
107
|
+
const cliConfiguredName = cliProviderConfigEnv?.trim() || null;
|
|
108
|
+
const configConfiguredName = configProviderConfigEnv?.trim() || null;
|
|
109
|
+
const unresolvedConfiguredName = cliConfiguredName ?? configConfiguredName ?? null;
|
|
110
|
+
|
|
111
|
+
const cliValue = readNonEmptyEnvValue(env, cliConfiguredName);
|
|
112
|
+
if (cliValue) {
|
|
113
|
+
return { providerConfigEnv: cliConfiguredName, providerConfigRef: cliValue };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const configValue = readNonEmptyEnvValue(env, configConfiguredName);
|
|
117
|
+
if (configValue) {
|
|
118
|
+
return { providerConfigEnv: configConfiguredName, providerConfigRef: configValue };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const aopFallback = readNonEmptyEnvValue(env, 'AOP_PROVIDER_CONFIG_ENV');
|
|
122
|
+
if (aopFallback) {
|
|
123
|
+
if (ENV_VAR_NAME_PATTERN.test(aopFallback)) {
|
|
124
|
+
const indirectValue = readNonEmptyEnvValue(env, aopFallback);
|
|
125
|
+
if (indirectValue) {
|
|
126
|
+
return { providerConfigEnv: aopFallback, providerConfigRef: indirectValue };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
providerConfigEnv: 'AOP_PROVIDER_CONFIG_ENV',
|
|
132
|
+
providerConfigRef: aopFallback,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
providerConfigEnv: unresolvedConfiguredName,
|
|
138
|
+
providerConfigRef: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
90
142
|
export const SUPPORTED_PROVIDERS: Set<string> = new Set(REGISTERED_PROVIDER_NAMES);
|
|
91
|
-
export const
|
|
143
|
+
export const LOCAL_CLI_PROVIDERS: Set<string> = new Set([
|
|
144
|
+
'codex',
|
|
145
|
+
'claude',
|
|
146
|
+
'kiro-cli',
|
|
147
|
+
'copilot',
|
|
148
|
+
'custom',
|
|
149
|
+
]);
|
|
150
|
+
export const CREDENTIAL_REQUIRED_PROVIDERS: Set<string> = new Set(['gemini']);
|
|
92
151
|
export type ProviderSelectionResolver = (input: ResolveSelectionInput) => ProviderSelection;
|
|
93
152
|
|
|
94
153
|
export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env, agentsConfig }) => {
|
|
@@ -98,11 +157,11 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
|
|
|
98
157
|
const model =
|
|
99
158
|
cli.agent_model || env.AOP_AGENT_MODEL || agentsConfig?.runtime?.default_model || null;
|
|
100
159
|
|
|
101
|
-
const providerConfigEnv =
|
|
102
|
-
cli.provider_config_env
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
160
|
+
const { providerConfigEnv, providerConfigRef } = resolveProviderCredentialReference(
|
|
161
|
+
cli.provider_config_env,
|
|
162
|
+
agentsConfig?.runtime?.provider_config_env,
|
|
163
|
+
env,
|
|
164
|
+
);
|
|
106
165
|
|
|
107
166
|
if (!provider) {
|
|
108
167
|
const error = new Error(ERROR_CODES.AGENT_PROVIDER_NOT_CONFIGURED) as AppError;
|
|
@@ -117,7 +176,7 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
|
|
|
117
176
|
throw error;
|
|
118
177
|
}
|
|
119
178
|
|
|
120
|
-
if (
|
|
179
|
+
if (CREDENTIAL_REQUIRED_PROVIDERS.has(provider) && !providerConfigRef) {
|
|
121
180
|
const error = new Error(ERROR_CODES.PROVIDER_AUTH_MISSING) as AppError;
|
|
122
181
|
error.code = ERROR_CODES.PROVIDER_AUTH_MISSING;
|
|
123
182
|
error.details = {
|
|
@@ -140,7 +199,7 @@ export const resolveProviderSelection: ProviderSelectionResolver = ({ cli, env,
|
|
|
140
199
|
provider,
|
|
141
200
|
model: model ?? `${provider}-default`,
|
|
142
201
|
provider_config_env: providerConfigEnv,
|
|
143
|
-
provider_config_ref:
|
|
202
|
+
provider_config_ref: providerConfigRef,
|
|
144
203
|
agent_config: agentConfig,
|
|
145
204
|
};
|
|
146
205
|
};
|