merge: main (CLI agent interface #1446) — renumber workflow_settings migration to 112 behind main's cli_sessions(110)/adapter(111), full-workspace literal sweep, i18n union
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.24.0",
|
||||
"@fusion/core": "workspace:*",
|
||||
"@fusion/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// to the agent.
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { redactSecrets } from "@fusion/core";
|
||||
|
||||
function debugLog(message: string): void {
|
||||
if (process.env.PI_ACP_DEBUG !== "1") return;
|
||||
@@ -113,31 +114,9 @@ export function spawnAgent(options: SpawnAgentOptions): ChildProcess {
|
||||
/** Maximum stderr bytes retained; older output is dropped to bound memory. */
|
||||
const STDERR_BUFFER_CEILING = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Redact token-like / auth patterns from text so auth errors don't leak
|
||||
* verbatim into the stderr buffer or logs (Risk S8). Best-effort: covers
|
||||
* bearer tokens, `Authorization:` header values, `key=`/`token=`/`secret=`
|
||||
* assignments, and long base64/hex secrets.
|
||||
*/
|
||||
export function redactSecrets(text: string): string {
|
||||
return (
|
||||
text
|
||||
// Authorization: Bearer <token> / Authorization: <token>
|
||||
.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[^\s,;"']+/gi, "$1$2[REDACTED]")
|
||||
// Bearer <token>
|
||||
.replace(/\b(bearer)\s+[A-Za-z0-9._\-+/=]+/gi, "$1 [REDACTED]")
|
||||
// key=... token=... secret=... password=... apikey=... (quoted or bare)
|
||||
.replace(
|
||||
/\b((?:api[_-]?key|key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*)("?)[^\s,;"']+\2/gi,
|
||||
"$1$2[REDACTED]$2",
|
||||
)
|
||||
// sk-/ghp_/github_pat_/xoxb-/AKIA-style long opaque tokens
|
||||
.replace(/\b(sk-|ghp_|gho_|github_pat_|xox[abpr]-|AKIA)[A-Za-z0-9_\-]{8,}/g, "[REDACTED]")
|
||||
// standalone long base64/hex secrets (>=32 chars)
|
||||
.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]")
|
||||
.replace(/\b[0-9a-fA-F]{32,}\b/g, "[REDACTED]")
|
||||
);
|
||||
}
|
||||
// Secret redaction (Risk S8) lives in @fusion/core so PTY/process owners share
|
||||
// one implementation; re-exported here to preserve this module's public surface.
|
||||
export { redactSecrets };
|
||||
|
||||
/**
|
||||
* Accumulate stderr into a bounded, secret-redacted buffer.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CeOrchestrator, type CeSessionExecutor } from "../session/orchestrator.js";
|
||||
import { makeHarness, type TestHarness } from "./_harness.js";
|
||||
|
||||
/**
|
||||
* U9 CE executor seam contract.
|
||||
*
|
||||
* Proves the `executor` option threads end-to-end on a REAL orchestrator (not a
|
||||
* scripted fake): the option set on deps is what `resolveExecutor()` returns,
|
||||
* and it defaults to the model backend. The cli-agent one-shot wiring itself is
|
||||
* engine-side (`@fusion/engine` runOneShotSession); this asserts the plugin
|
||||
* carries the choice through, per the plugin-skills option-threading learning.
|
||||
*/
|
||||
let h: TestHarness;
|
||||
beforeEach(() => {
|
||||
h = makeHarness();
|
||||
});
|
||||
afterEach(() => {
|
||||
h.close();
|
||||
});
|
||||
|
||||
function makeOrch(executor?: CeSessionExecutor) {
|
||||
return new CeOrchestrator({
|
||||
ctx: h.ctx,
|
||||
createInteractiveAiSession: vi.fn(async () => ({ session: {} as never })),
|
||||
projectRoot: h.projectRoot,
|
||||
executor,
|
||||
});
|
||||
}
|
||||
|
||||
describe("CE executor seam (U9)", () => {
|
||||
it("defaults to the model backend when no executor option is supplied", () => {
|
||||
expect(makeOrch().resolveExecutor()).toEqual({ kind: "model" });
|
||||
});
|
||||
|
||||
it("threads a cli-agent executor selection through deps → resolver", () => {
|
||||
const orch = makeOrch({ kind: "cli-agent", adapterId: "claude-code" });
|
||||
expect(orch.resolveExecutor()).toEqual({ kind: "cli-agent", adapterId: "claude-code" });
|
||||
});
|
||||
});
|
||||
@@ -85,6 +85,20 @@ export class CeTurnTimeoutError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Which session backend a CE stage runs on (U9 seam).
|
||||
*
|
||||
* - `model` (default): the model-backed interactive AI session (existing path).
|
||||
* - `cli-agent`: a CLI agent adapter, run as a read-only one-shot per stage.
|
||||
*
|
||||
* The CE plugin threads this choice end-to-end (deps → resolver) per the
|
||||
* plugin-skills option-threading learning; the cli-agent one-shot wiring itself
|
||||
* lives engine-side in `@fusion/engine` (`runOneShotSession`).
|
||||
*/
|
||||
export type CeSessionExecutor =
|
||||
| { kind: "model" }
|
||||
| { kind: "cli-agent"; adapterId: string };
|
||||
|
||||
export interface OrchestratorDeps {
|
||||
ctx: PluginContext;
|
||||
/**
|
||||
@@ -96,6 +110,19 @@ export interface OrchestratorDeps {
|
||||
projectRoot?: string;
|
||||
/** Override the per-turn timeout (ms). */
|
||||
turnTimeoutMs?: number;
|
||||
/**
|
||||
* Session backend selector (U9). Defaults to `{ kind: "model" }`. When set to
|
||||
* `{ kind: "cli-agent", adapterId }`, CE sessions select the CLI-agent
|
||||
* one-shot executor. Threaded through to `resolveExecutor()` so callers can
|
||||
* route a CE stage onto a CLI adapter.
|
||||
*
|
||||
* DEVIATION (see U9 report): the cli-agent branch of the *live* CE stage loop
|
||||
* (replacing the interactive factory with one-shot turns inside
|
||||
* `startStage`/`continueStage`) is not yet wired — only the option seam and
|
||||
* its resolver contract land here. The engine-side one-shot runner is ready;
|
||||
* the remaining work is invoking it from the stage loop.
|
||||
*/
|
||||
executor?: CeSessionExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,6 +203,7 @@ export class CeOrchestrator {
|
||||
private readonly factory: CreateInteractiveAiSessionFactory | undefined;
|
||||
private readonly projectRoot: string;
|
||||
private readonly turnTimeoutMs: number;
|
||||
private readonly executor: CeSessionExecutor;
|
||||
/** Live in-memory session handles keyed by ce_session id. */
|
||||
private readonly live = new Map<string, InteractiveAiSession>();
|
||||
/** Mid-turn working output per session (transient; flushed to history on settle). */
|
||||
@@ -194,6 +222,17 @@ export class CeOrchestrator {
|
||||
this.factory = deps.createInteractiveAiSession ?? deps.ctx.createInteractiveAiSession;
|
||||
this.projectRoot = deps.projectRoot ?? deps.ctx.taskStore.getRootDir();
|
||||
this.turnTimeoutMs = deps.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
|
||||
this.executor = deps.executor ?? { kind: "model" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the session backend for a CE stage (U9 seam). Threaded from
|
||||
* `OrchestratorDeps.executor`; defaults to the model-backed interactive
|
||||
* session. Exposed so the seam contract is directly assertable in tests and so
|
||||
* future stage-loop wiring has a single resolution point.
|
||||
*/
|
||||
resolveExecutor(): CeSessionExecutor {
|
||||
return this.executor;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -743,10 +743,10 @@ describe("RoadmapStore", () => {
|
||||
});
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 110 after init", () => {
|
||||
it("schema version is 112 after init", () => {
|
||||
// Tracks @fusion/core's SCHEMA_VERSION (the roadmap store layers on core's
|
||||
// Database). Bump this in lockstep when core adds a migration.
|
||||
expect(db.getSchemaVersion()).toBe(110);
|
||||
expect(db.getSchemaVersion()).toBe(112);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user