mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
feat(context): prepare isolated Claude native generations
This commit is contained in:
1 parent
c51842974a
commit
f9196d3762
8 files changed
+228
-82
No files matched your search
@@ -68,7 +68,15 @@ The task call sends the query and selected reference content on standard input t
|
||||
|
||||
`profile start` is an explicit terminal-only boundary. It revalidates the store and native generation, then launches the pinned Codex binary with inherited terminal capabilities and the isolated home. The bootstrap tells the active agent to resolve context at material task boundaries through bounded structured stdin. It remains prompt-advisory, grants no tools or permissions, and persists no task prose or selected skill bodies. Authentication must be completed separately inside the isolated home; the start path does not inherit or copy provider credentials.
|
||||
|
||||
Switching the managed profile makes the old native generation stale until `prepare-native` succeeds. To undo a switch, first `rollback` the managed store, then use `native-rollback` with both roots. `native-recover` handles a retained interruption journal without deleting provider data. Existing sessions retain their original context. These commands support isolated Codex generations, not migration of an existing global installation or native activation for other providers.
|
||||
## Isolated native Claude generations
|
||||
|
||||
A managed store with `--target claude` uses the same `prepare-native`, `native-status`, `native-rollback`, `native-recover`, `run` and `start` commands. Claude Code loads the carrier for each session with `--plugin-dir`; nothing is installed, no marketplace is registered and no settings are written. The generation holds a private `home/.claude` used as `CLAUDE_CONFIG_DIR`, an empty project and a verified copy of the carrier in `plugin/`. Every Claude call sets `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` and `CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL`, so the isolated home gains no other plugins.
|
||||
|
||||
Preparation admits only Claude Code 2.1.292. It requires a clean `claude plugin validate --json` report, exactly one session plugin (`ecc-context-carrier@inline`, enabled, at the generation's `plugin/` path) from `claude --plugin-dir … plugin list --json`, and a `claude --plugin-dir … plugin details` inventory whose skills equal the carrier selection with zero agents, hooks, MCP servers and LSP servers. `details` has no JSON form; the pinned version fixes its layout and an unparseable report fails closed. The copied files must match the carrier digests after the provider calls.
|
||||
|
||||
The receipt records the host's always-on projection as `nativeObservation` (`surface: plugin-always-on-listing`, `method: claude-plugin-details@2.1.292`, `approximate: true`). It is Claude Code's own estimate of the listing cost of this one plugin, not whole-context usage. `native-status` checks file integrity of the plugin copy, `CLAUDE.md`, user settings, skills, agents and commands in the isolated home; provider runtime state such as `.claude.json` and plugin bookkeeping is outside those controls, and plugin registration is verified again at each preparation.
|
||||
|
||||
Switching the managed profile makes the old native generation stale until `prepare-native` succeeds. To undo a switch, first `rollback` the managed store, then use `native-rollback` with both roots. `native-recover` handles a retained interruption journal without deleting provider data. Existing sessions retain their original context. These commands support isolated Codex and Claude generations, not migration of an existing global installation or native activation for other providers.
|
||||
|
||||
Discovery evidence comes from the generation's empty project. Task launch inherits the caller's task working directory, whose repository instructions and native configuration may add context or affect policy. Native readiness attests the isolated home's recorded inventory and integrity, not the complete context or permissions of every possible task directory.
|
||||
|
||||
|
||||
@@ -88,4 +88,13 @@ Current combined verification after recovery:
|
||||
- The default sandbox checkout's 5,000-path capture limit truncated a real Tier 1 install diff and failed closed. The reviewed ECC-029 sandbox implementation raises the bounded cap to 50,000, passes its 26-case boundary suite, and produced both final reports. The driver receipt binds its 51-file implementation digest `a84e09ab848b8cd05f33792c13734f7aabe16bfe16d50d8f8292eb5261a93c3a`.
|
||||
- No real AI outcome call ran because `CODEX_API_KEY` was absent. Host ChatGPT authentication was neither copied nor exposed to the disposable evaluator.
|
||||
|
||||
## Isolated Claude native generations
|
||||
|
||||
Session-only Claude discovery adapts the ownership and receipt intent of #2788 to the existing native store; it never writes a marketplace or user settings.
|
||||
|
||||
- RED: 22 cases fail before the provider adapter (preview, prepare, version gate, nine discovery corruptions, details mismatch, static integrity, runtime-state tolerance, provider-home roots, Codex-to-Claude staleness, task launch and interactive start).
|
||||
- GREEN: all 22 pass. Codex native (39), interactive (8), launch (12), auto launch (5), evaluation (31), store (31) and profile CLI suites pass unchanged; Codex receipts keep their shape, and evaluator Claude launches without a plugin directory keep their arguments and environment.
|
||||
- Credential-free real run on Linux x64 with Claude Code 2.1.292 and the actual registry: Lean prepared ready with three skills and a 398-token always-on projection; Full prepared ready with 293 skills and 35,658 tokens; Full excluding `skill:python-patterns` verified 292 skills without it at 35,561 tokens. Switching profiles reported `stale`, and native rollback restored the Lean generation. `run --dry-run` resolved the pinned binary with `--plugin-dir`; `start --dry-run` proposed the Claude generation.
|
||||
- No authenticated model call, interactive turn or native skill invocation ran. The projection is the host's estimate for the plugin listing, not whole-context truth.
|
||||
|
||||
These boundaries keep the shipped behavior distinct from the M1 release gate. Authenticated outcome observations, a complete Tier 2 disk diff, live-install migration, other-provider activation, whole-context token truth and release defaults remain unverified until their explicit prerequisites are available.
|
||||
@@ -119,8 +119,9 @@ function execute(options) {
|
||||
stateRoot: options['state-root'], nativeRoot: options['native-root'] }) : null;
|
||||
if (native && !native.ready) throw new Error('Prepare or recover the native generation before launching');
|
||||
return { launch: require('./context-profile-launch').launchTaskContext({ ...launchInput, dryRun: options.dryRun,
|
||||
nativeEnvironment: native ? { home: native.home, codexHome: native.codexHome,
|
||||
codexPath: native.codexPath, executableDigest: native.executableDigest } : null,
|
||||
nativeEnvironment: native ? { home: native.home, executableDigest: native.executableDigest,
|
||||
...(native.target === 'claude' ? { claudeConfigDir: native.claudeConfigDir, claudePath: native.claudePath, pluginDir: native.pluginDir }
|
||||
: { codexHome: native.codexHome, codexPath: native.codexPath }) } : null,
|
||||
assertCurrent() {
|
||||
if (stored) {
|
||||
const current = require('./context-profile-store').getStoreStatus({ stateRoot: options['state-root'] });
|
||||
@@ -162,7 +163,7 @@ function run(argv) {
|
||||
const options = parse(argv);
|
||||
const value = execute(options);
|
||||
return { schemaVersion: 'ecc.profile-operation.v1', status: (value.launch?.status === 'failed' || value.interactive?.status === 'failed') ? 'error' : 'success',
|
||||
summary: options.command === 'start' ? 'Opt-in interactive Codex uses the verified isolated generation and inherited terminal. Context selection remains advisory.'
|
||||
summary: options.command === 'start' ? 'Opt-in interactive session uses the verified isolated generation and inherited terminal. Context selection remains advisory.'
|
||||
: options.command === 'run' ? 'Task launch uses selected context and the provider configuration. Inspect the launch result.'
|
||||
: options.command === 'resolve' ? 'Task context resolved within the selected profile.'
|
||||
: 'Managed profile generation inspected. Native activation is a separate provider boundary.',
|
||||
|
||||
@@ -80,18 +80,19 @@ function startInteractiveProfile({ stateRoot, nativeRoot, dryRun = false } = {},
|
||||
if (carrier.carrierDigest !== stored.carrierDigest) throw new Error('Stored profile source is stale; set and prepare the current generation before starting');
|
||||
const current = native.getNativeProfileStatus(input);
|
||||
if (!current.ready || current.revision !== prepared.revision) throw new Error('Native generation changed before interactive launch');
|
||||
const env = { PATH: process.env.PATH, HOME: current.home, USERPROFILE: current.home,
|
||||
CODEX_HOME: current.codexHome, LANG: 'C.UTF-8' };
|
||||
const claude = current.target === 'claude';
|
||||
const env = { PATH: process.env.PATH, HOME: current.home, USERPROFILE: current.home, LANG: 'C.UTF-8',
|
||||
...(claude ? { CLAUDE_CONFIG_DIR: current.claudeConfigDir, ...native.CLAUDE_ISOLATION_ENV } : { CODEX_HOME: current.codexHome }) };
|
||||
// Terminal capabilities are needed by the TUI; credentials and provider overrides are not inherited.
|
||||
for (const key of ['TERM', 'COLORTERM', 'TERM_PROGRAM', 'SystemRoot']) {
|
||||
if (process.env[key]) env[key] = process.env[key];
|
||||
}
|
||||
const bootstrapDigest = io.hash(io.read(path.join(current.codexHome, 'AGENTS.md')));
|
||||
const result = (dependencies.execute || spawnSync)(current.codexPath, [], {
|
||||
const bootstrapDigest = io.hash(io.read(claude ? path.join(current.claudeConfigDir, 'CLAUDE.md') : path.join(current.codexHome, 'AGENTS.md')));
|
||||
const result = (dependencies.execute || spawnSync)(current.executable, claude ? ['--plugin-dir', current.pluginDir] : [], {
|
||||
cwd: process.cwd(), env, shell: false, stdio: 'inherit' });
|
||||
return { schemaVersion: 'ecc.interactive-profile.v1', status: result.error || result.status !== 0 ? 'failed' : 'exited',
|
||||
launched: !result.error, exitCode: result.status ?? null, signal: result.signal || null,
|
||||
...(result.error ? { error: 'Native interactive Codex could not be started' } : {}),
|
||||
...(result.error ? { error: `Native interactive ${claude ? 'Claude' : 'Codex'} could not be started` } : {}),
|
||||
nativeRevision: current.revision, providerVersion: current.providerVersion,
|
||||
bootstrapDigest,
|
||||
credentialsCopied: false, taskSuccess: 'unverified', enforcement: 'prompt-advisory' };
|
||||
|
||||
@@ -9,6 +9,7 @@ function isolatedEnvironment(nativeEnvironment) {
|
||||
USERPROFILE: nativeEnvironment.home,
|
||||
...(nativeEnvironment.codexHome ? { CODEX_HOME: nativeEnvironment.codexHome } : {}),
|
||||
...(nativeEnvironment.claudeConfigDir ? { CLAUDE_CONFIG_DIR: nativeEnvironment.claudeConfigDir } : {}),
|
||||
...(nativeEnvironment.pluginDir ? require('./context-profile-native').CLAUDE_ISOLATION_ENV : {}),
|
||||
TMPDIR: nativeEnvironment.home, LANG: 'C.UTF-8' };
|
||||
if (process.platform === 'win32' && process.env.SystemRoot) env.SystemRoot = process.env.SystemRoot;
|
||||
return env;
|
||||
@@ -22,11 +23,11 @@ function launchTaskContext({ task, target = 'codex', dryRun = false, execute = s
|
||||
if (!Object.hasOwn(adapters, target)) throw new Error(`Unsupported task launcher target: ${target}`);
|
||||
if (!task || typeof task.query !== 'string' || !task.query.trim()) throw new Error('Task launch requires a non-empty query');
|
||||
if (nativeEnvironment) {
|
||||
const launchKeys = target === 'claude'
|
||||
? { directory: nativeEnvironment.claudeConfigDir, executable: nativeEnvironment.claudePath }
|
||||
: { directory: nativeEnvironment.codexHome, executable: nativeEnvironment.codexPath };
|
||||
if (!path.isAbsolute(nativeEnvironment.home || '') || !path.isAbsolute(launchKeys.directory || '')
|
||||
|| !path.isAbsolute(launchKeys.executable || '')
|
||||
const launchPaths = target === 'claude'
|
||||
? [nativeEnvironment.claudeConfigDir, nativeEnvironment.claudePath,
|
||||
...(nativeEnvironment.pluginDir === undefined ? [] : [nativeEnvironment.pluginDir])]
|
||||
: [nativeEnvironment.codexHome, nativeEnvironment.codexPath];
|
||||
if (![nativeEnvironment.home, ...launchPaths].every(value => path.isAbsolute(value || ''))
|
||||
|| !/^[a-f0-9]{64}$/.test(nativeEnvironment.executableDigest || '')) throw new Error('Invalid isolated native launch environment');
|
||||
}
|
||||
let selection = bare
|
||||
@@ -34,7 +35,8 @@ function launchTaskContext({ task, target = 'codex', dryRun = false, execute = s
|
||||
selectionMode: 'manual', reason: 'bare-baseline', receipt: { bindingDigest: 'bare' } }
|
||||
: resolveTaskContext({ ...selectionOptions, task, target, load: !dryRun });
|
||||
const adapter = { ...adapters[target],
|
||||
...(nativeEnvironment ? { command: nativeEnvironment.codexPath || nativeEnvironment.claudePath } : {}) };
|
||||
...(nativeEnvironment ? { command: target === 'claude' ? nativeEnvironment.claudePath : nativeEnvironment.codexPath } : {}),
|
||||
...(nativeEnvironment?.pluginDir && target === 'claude' ? { args: ['--plugin-dir', nativeEnvironment.pluginDir, ...adapters.claude.args] } : {}) };
|
||||
function verifyLaunch() {
|
||||
assertCurrent();
|
||||
if (nativeEnvironment && require('./context-profile-native-executable').fingerprintExecutable(adapter.command).digest
|
||||
|
||||
@@ -14,11 +14,14 @@ function nativeFormat(header) {
|
||||
|| header.subarray(0, 2).toString() === 'MZ';
|
||||
}
|
||||
|
||||
function resolveExecutable(command) {
|
||||
const LABELS = { codex: 'Codex', claude: 'Claude Code' };
|
||||
|
||||
function resolveExecutable(command, name = 'codex') {
|
||||
if (!Object.hasOwn(LABELS, name)) throw new Error(`Unsupported native executable: ${name}`);
|
||||
const candidate = path.isAbsolute(command) ? command : (process.env.PATH || '').split(path.delimiter)
|
||||
.filter(directory => path.isAbsolute(directory)).map(directory => path.join(directory, process.platform === 'win32' ? 'codex.exe' : 'codex'))
|
||||
.filter(directory => path.isAbsolute(directory)).map(directory => path.join(directory, process.platform === 'win32' ? `${name}.exe` : name))
|
||||
.find(file => fs.existsSync(file));
|
||||
if (!candidate) throw new Error('Native Codex executable was not found');
|
||||
if (!candidate) throw new Error(`Native ${LABELS[name]} executable was not found`);
|
||||
let executable = fs.realpathSync(candidate);
|
||||
const before = io.inspect(executable);
|
||||
if (!before.stat.isFile() || before.stat.nlink !== 1 || before.stat.size < 4 || before.stat.size > MAX_BYTES) {
|
||||
@@ -33,7 +36,7 @@ function resolveExecutable(command) {
|
||||
} finally { fs.closeSync(fd); }
|
||||
if (!nativeFormat(header)) {
|
||||
// Supported npm distribution: bind its platform binary, never only its JS shim.
|
||||
if (path.basename(executable) !== 'codex.js') throw new Error('Native adapter requires a native Codex executable');
|
||||
if (name !== 'codex' || path.basename(executable) !== 'codex.js') throw new Error(`Native adapter requires a native ${LABELS[name]} executable`);
|
||||
const packageName = `@openai/codex-${process.platform}-${process.arch}`;
|
||||
let manifest;
|
||||
try { manifest = createRequire(executable).resolve(`${packageName}/package.json`); }
|
||||
|
||||
@@ -14,15 +14,35 @@ const { digestObject, stableStringify, validateSchema } = require('./context-pro
|
||||
const { discoverSync } = require('./context-profile-native-discovery');
|
||||
const { fingerprintExecutable, resolveExecutable } = require('./context-profile-native-executable');
|
||||
|
||||
const VERSION = '0.154.0';
|
||||
// 0.155.1: credential-free native-probe verified Lean, include, Full exclusion and resource relocation.
|
||||
const SUPPORTED_VERSIONS = ['0.154.0', '0.155.1'];
|
||||
const DIGEST = /^[a-f0-9]{64}$/;
|
||||
const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
||||
const KEYS = new Set(['stateRoot', 'nativeRoot', 'expectedRevision', 'expectedCarrierDigest', 'codexPath']);
|
||||
const CONTROLS = ['marketplace', 'project', 'home/.agents', 'home/.codex/config.toml',
|
||||
'home/.codex/AGENTS.md', 'home/.codex/AGENTS.override.md', 'home/.codex/hooks.json',
|
||||
'home/.codex/requirements.toml', 'home/.codex/plugins', 'home/.codex/skills'];
|
||||
const KEYS = new Set(['stateRoot', 'nativeRoot', 'expectedRevision', 'expectedCarrierDigest', 'codexPath', 'claudePath']);
|
||||
const PLUGIN = 'ecc-context-carrier';
|
||||
// Claude loads the carrier per session with --plugin-dir; nothing is installed.
|
||||
// Official marketplace auto-install would add plugins to the isolated home.
|
||||
const CLAUDE_ISOLATION_ENV = Object.freeze({ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
||||
CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: '1' });
|
||||
const PROVIDERS = Object.freeze({
|
||||
codex: { label: 'Codex', command: 'codex', executableKey: 'codexPath',
|
||||
// 0.155.1: credential-free native-probe verified Lean, include, Full exclusion and resource relocation.
|
||||
versions: ['0.154.0', '0.155.1'], versionOutput: version => `codex-cli ${version}`,
|
||||
bootstrapPath: 'home/.codex/AGENTS.md',
|
||||
controls: ['marketplace', 'project', 'home/.agents', 'home/.codex/config.toml',
|
||||
'home/.codex/AGENTS.md', 'home/.codex/AGENTS.override.md', 'home/.codex/hooks.json',
|
||||
'home/.codex/requirements.toml', 'home/.codex/plugins', 'home/.codex/skills'] },
|
||||
// 2.1.292: credential-free probe verified Lean and Full session-only discovery and host projections.
|
||||
claude: { label: 'Claude', command: 'claude', executableKey: 'claudePath',
|
||||
versions: ['2.1.292'], versionOutput: version => `${version} (Claude Code)`,
|
||||
bootstrapPath: 'home/.claude/CLAUDE.md',
|
||||
// Provider runtime state (.claude.json, projects, backups, plugin bookkeeping)
|
||||
// is not skill discovery state; plugin registration is re-verified on prepare.
|
||||
controls: ['plugin', 'project', 'home/.claude/CLAUDE.md', 'home/.claude/settings.json',
|
||||
'home/.claude/settings.local.json', 'home/.claude/skills', 'home/.claude/agents', 'home/.claude/commands'] },
|
||||
});
|
||||
const providerFor = target => {
|
||||
if (!Object.hasOwn(PROVIDERS, target)) throw new Error('Native preparation requires a configured, recovered Codex or Claude managed store');
|
||||
return PROVIDERS[target];
|
||||
};
|
||||
const exists = file => Boolean(fs.lstatSync(file, { throwIfNoEntry: false }));
|
||||
const equal = (a, b) => stableStringify(a) === stableStringify(b);
|
||||
const inside = (a, b) => a === b || a.startsWith(`${b}${path.sep}`);
|
||||
@@ -50,9 +70,10 @@ function inputs(options) {
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new Error('Native profile options must be an object');
|
||||
for (const key of Object.keys(options)) if (!KEYS.has(key)) throw new Error(`Unknown native profile option: ${key}`);
|
||||
const { nativeRoot, stateRoot } = options;
|
||||
const providerHomes = [path.join(os.homedir(), '.codex'), process.env.CODEX_HOME,
|
||||
path.join(os.homedir(), '.claude'), process.env.CLAUDE_CONFIG_DIR].filter(Boolean);
|
||||
if (typeof nativeRoot !== 'string' || !path.isAbsolute(nativeRoot) || path.resolve(nativeRoot) !== nativeRoot
|
||||
|| nativeRoot === path.parse(nativeRoot).root || nativeRoot === os.homedir()
|
||||
|| nativeRoot === path.join(os.homedir(), '.codex') || nativeRoot === process.env.CODEX_HOME) {
|
||||
|| nativeRoot === path.parse(nativeRoot).root || nativeRoot === os.homedir() || providerHomes.includes(nativeRoot)) {
|
||||
throw new Error('nativeRoot must be an explicit dedicated isolated root');
|
||||
}
|
||||
if (typeof stateRoot !== 'string' || !path.isAbsolute(stateRoot)) throw new Error('Managed stateRoot is required');
|
||||
@@ -62,7 +83,7 @@ function inputs(options) {
|
||||
const canonicalNative = exists(nativeRoot) ? fs.realpathSync(nativeRoot)
|
||||
: path.join(fs.realpathSync(path.dirname(nativeRoot)), path.basename(nativeRoot));
|
||||
const normalized = value => process.platform === 'win32' || process.platform === 'darwin' ? value.toLowerCase() : value;
|
||||
const forbidden = [os.homedir(), path.join(os.homedir(), '.codex'), process.env.CODEX_HOME].filter(Boolean);
|
||||
const forbidden = [os.homedir(), ...providerHomes];
|
||||
if (forbidden.some(file => normalized(exists(file) ? fs.realpathSync(file) : file) === normalized(canonicalNative))) {
|
||||
throw new Error('nativeRoot must be an explicit dedicated isolated root');
|
||||
}
|
||||
@@ -73,10 +94,14 @@ function inputs(options) {
|
||||
throw new Error('Invalid native expected revision');
|
||||
}
|
||||
if (options.expectedCarrierDigest !== undefined && !DIGEST.test(options.expectedCarrierDigest)) throw new Error('Invalid native expected carrier digest');
|
||||
if (options.codexPath !== undefined && (typeof options.codexPath !== 'string'
|
||||
|| (options.codexPath !== 'codex' && !path.isAbsolute(options.codexPath)))) throw new Error('codexPath must be codex or an absolute executable path');
|
||||
for (const { command, executableKey } of Object.values(PROVIDERS)) {
|
||||
const value = options[executableKey];
|
||||
if (value !== undefined && (typeof value !== 'string' || (value !== command && !path.isAbsolute(value)))) {
|
||||
throw new Error(`${executableKey} must be ${command} or an absolute executable path`);
|
||||
}
|
||||
}
|
||||
io.inspect(nativeRoot, true);
|
||||
return { ...options, codexPath: options.codexPath || 'codex' };
|
||||
return { ...options, codexPath: options.codexPath || 'codex', claudePath: options.claudePath || 'claude' };
|
||||
}
|
||||
|
||||
function owner(options, create = false) {
|
||||
@@ -96,9 +121,10 @@ function owner(options, create = false) {
|
||||
|
||||
function currentStore(options) {
|
||||
const current = getStoreStatus({ stateRoot: options.stateRoot });
|
||||
if (!current.configured || current.recoveryRequired || current.target !== 'codex') {
|
||||
throw new Error('Native preparation requires a configured, recovered Codex managed store');
|
||||
if (!current.configured || current.recoveryRequired) {
|
||||
throw new Error('Native preparation requires a configured, recovered Codex or Claude managed store');
|
||||
}
|
||||
providerFor(current.target);
|
||||
if (options.expectedCarrierDigest && current.carrierDigest !== options.expectedCarrierDigest) throw new Error('Managed carrier digest changed since preview');
|
||||
return current;
|
||||
}
|
||||
@@ -123,8 +149,8 @@ function readState(options) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function snapshot(root) {
|
||||
return CONTROLS.map(relative => {
|
||||
function snapshot(root, provider) {
|
||||
return provider.controls.map(relative => {
|
||||
const file = path.join(root, relative);
|
||||
if (relative === 'home/.codex/config.toml') {
|
||||
// Provider-owned runtime config: compare discovery-relevant state only
|
||||
@@ -148,36 +174,48 @@ function snapshot(root) {
|
||||
function loadReceipt(options, state, { allowRefresh = false } = {}) {
|
||||
const root = generation(options, state.generationId);
|
||||
const receipt = io.readJson(path.join(root, 'receipt.json'));
|
||||
// Receipts written before Claude support carry no target and are Codex receipts.
|
||||
const target = receipt.target ?? 'codex';
|
||||
if (digestObject(receipt) !== state.generationReceiptDigest || receipt.schemaVersion !== 'ecc.native-context-receipt.v1'
|
||||
|| receipt.generationId !== state.generationId || !SUPPORTED_VERSIONS.includes(receipt.providerVersion)
|
||||
|| receipt.generationId !== state.generationId || !Object.hasOwn(PROVIDERS, target)
|
||||
|| !PROVIDERS[target].versions.includes(receipt.providerVersion)
|
||||
|| receipt.bindingDigest !== digestObject({ nativeRoot: options.nativeRoot, stateRoot: options.stateRoot })) {
|
||||
throw new Error('Native receipt integrity failed');
|
||||
}
|
||||
const provider = PROVIDERS[target];
|
||||
const carrier = io.readJson(path.join(root, 'carrier.json'));
|
||||
validateSchema(carrier, 'context-carrier.schema.json');
|
||||
const { carrierDigest, ...body } = carrier;
|
||||
if (carrierDigest !== receipt.carrierDigest || digestObject(body) !== carrierDigest) throw new Error('Native carrier digest integrity failed');
|
||||
if (!equal(snapshot(root), receipt.controls)) throw new Error('Native discovery configuration or skill bytes changed');
|
||||
if (!equal(snapshot(root, provider), receipt.controls)) throw new Error('Native discovery configuration or skill bytes changed');
|
||||
if (!allowRefresh && (!receipt.executable || !equal(fingerprintExecutable(receipt.executable.path), receipt.executable))) {
|
||||
throw new Error('Native Codex executable changed since preparation');
|
||||
throw new Error(`Native ${provider.label} executable changed since preparation`);
|
||||
}
|
||||
if (receipt.bootstrap) {
|
||||
if (receipt.bootstrap.stateRoot !== options.stateRoot || receipt.bootstrap.nativeRoot !== options.nativeRoot
|
||||
|| receipt.bootstrap.carrierDigest !== receipt.carrierDigest) throw new Error('Interactive root binding integrity failed');
|
||||
if (!allowRefresh) require('./context-profile-interactive').verifyBootstrap(receipt.bootstrap);
|
||||
}
|
||||
return { receipt, carrier, root };
|
||||
return { receipt, carrier, root, target };
|
||||
}
|
||||
|
||||
function providerPaths(target, root) {
|
||||
if (target === 'claude') {
|
||||
return { codexHome: null, claudeConfigDir: root && path.join(root, 'home/.claude'), pluginDir: root && path.join(root, 'plugin') };
|
||||
}
|
||||
return { codexHome: root && path.join(root, 'home/.codex') };
|
||||
}
|
||||
|
||||
function response(options, state, current, pending = false, allowRefresh = false) {
|
||||
const base = { schemaVersion: 'ecc.native-context-status.v1', nativeRoot: options.nativeRoot,
|
||||
stateRoot: options.stateRoot, active: false, ready: false, revision: state?.revision || 0,
|
||||
status: pending ? 'recovery-required' : 'unconfigured', target: 'codex',
|
||||
providerVersion: VERSION, home: null, codexHome: null, carrierDigest: null, storeRevision: null,
|
||||
status: pending ? 'recovery-required' : 'unconfigured', target: current.target,
|
||||
providerVersion: providerFor(current.target).versions[0], home: null, ...providerPaths(current.target, null),
|
||||
carrierDigest: null, storeRevision: null,
|
||||
currentStoreRevision: current.revision, currentCarrierDigest: current.carrierDigest,
|
||||
discovery: 'unobserved', currentSessionChanged: false, credentialsCopied: false };
|
||||
if (!state) return base;
|
||||
const { receipt, carrier, root } = loadReceipt(options, state, { allowRefresh });
|
||||
const { receipt, carrier, root, target } = loadReceipt(options, state, { allowRefresh });
|
||||
let bindingsMatch = true;
|
||||
if (allowRefresh) {
|
||||
try {
|
||||
@@ -185,12 +223,14 @@ function response(options, state, current, pending = false, allowRefresh = false
|
||||
if (receipt.bootstrap) require('./context-profile-interactive').verifyBootstrap(receipt.bootstrap);
|
||||
} catch { bindingsMatch = false; }
|
||||
}
|
||||
const matches = state.storeRevision === current.revision && receipt.carrierDigest === current.carrierDigest;
|
||||
const matches = state.storeRevision === current.revision && receipt.carrierDigest === current.carrierDigest
|
||||
&& target === current.target;
|
||||
return { ...base, status: pending ? 'recovery-required' : !bindingsMatch ? 'refresh-required' : matches ? 'ready' : 'stale',
|
||||
ready: matches && bindingsMatch && !pending, providerVersion: receipt.providerVersion, bootstrap: receipt.bootstrap || null,
|
||||
home: path.join(root, 'home'), codexHome: path.join(root, 'home/.codex'),
|
||||
ready: matches && bindingsMatch && !pending, target, providerVersion: receipt.providerVersion, bootstrap: receipt.bootstrap || null,
|
||||
home: path.join(root, 'home'), ...providerPaths(target, root),
|
||||
carrierDigest: receipt.carrierDigest, storeRevision: state.storeRevision,
|
||||
codexPath: receipt.executable.path, executable: receipt.executable.path, executableDigest: receipt.executable.digest,
|
||||
[PROVIDERS[target].executableKey]: receipt.executable.path, executable: receipt.executable.path, executableDigest: receipt.executable.digest,
|
||||
...(receipt.nativeObservation ? { nativeObservation: receipt.nativeObservation } : {}),
|
||||
selectedIds: carrier.selectedIds, discovery: 'verified', evidenceScope: 'native-preparation-with-current-file-integrity',
|
||||
activation: 'isolated-home-ready-for-new-session', modelInvocation: 'unobserved' };
|
||||
}
|
||||
@@ -208,33 +248,90 @@ function previewNativeProfile(input) {
|
||||
exists(path.join(options.nativeRoot, 'pending.json')) || exists(path.join(options.nativeRoot, '.lock')), true)
|
||||
: response(options, null, current);
|
||||
if (options.expectedRevision !== undefined && options.expectedRevision !== before.revision) throw new Error('Native revision changed since preview');
|
||||
const provider = providerFor(current.target);
|
||||
return { ...before, status: 'proposed', ready: false, proposedCarrierDigest: current.carrierDigest,
|
||||
proposedStoreRevision: current.revision, requiredProviderVersion: VERSION, supportedProviderVersions: [...SUPPORTED_VERSIONS] };
|
||||
proposedStoreRevision: current.revision, requiredProviderVersion: provider.versions[0], supportedProviderVersions: [...provider.versions] };
|
||||
}
|
||||
|
||||
function environment(root) {
|
||||
const env = { PATH: process.env.PATH, HOME: path.join(root, 'home'), CODEX_HOME: path.join(root, 'home/.codex'), LANG: 'C.UTF-8' };
|
||||
function environment(root, target) {
|
||||
const env = { PATH: process.env.PATH, HOME: path.join(root, 'home'), LANG: 'C.UTF-8',
|
||||
...(target === 'claude' ? { CLAUDE_CONFIG_DIR: path.join(root, 'home/.claude'), ...CLAUDE_ISOLATION_ENV }
|
||||
: { CODEX_HOME: path.join(root, 'home/.codex') }) };
|
||||
if (process.platform === 'win32' && process.env.SystemRoot) env.SystemRoot = process.env.SystemRoot;
|
||||
return env;
|
||||
}
|
||||
|
||||
function command(options, root, args, dependencies) {
|
||||
if (options.executableBinding && !equal(fingerprintExecutable(options.codexPath), options.executableBinding)) {
|
||||
const { label } = providerFor(options.target);
|
||||
if (options.executableBinding && !equal(fingerprintExecutable(options.executablePath), options.executableBinding)) {
|
||||
throw new Error('Native executable changed before provider call');
|
||||
}
|
||||
const result = (dependencies.execute || spawnSync)(options.codexPath, args, {
|
||||
cwd: path.join(root, 'project'), env: environment(root), encoding: 'utf8', shell: false,
|
||||
const result = (dependencies.execute || spawnSync)(options.executablePath, args, {
|
||||
cwd: path.join(root, 'project'), env: environment(root, options.target), encoding: 'utf8', shell: false,
|
||||
timeout: 30000, killSignal: 'SIGKILL', maxBuffer: 2 * 1024 * 1024 });
|
||||
if (result.error || result.status !== 0) throw new Error('Native Codex command failed; isolated attempt retained for recovery');
|
||||
if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > 2 * 1024 * 1024) throw new Error('Native Codex command output exceeded its bound');
|
||||
if (result.error || result.status !== 0) throw new Error(`Native ${label} command failed; isolated attempt retained for recovery`);
|
||||
if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > 2 * 1024 * 1024) throw new Error(`Native ${label} command output exceeded its bound`);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function verifyFiles(directory, carrier) {
|
||||
const observed = io.inventory(directory).files.sort((a, b) => a.path.localeCompare(b.path));
|
||||
const expected = carrier.files.map(file => ({ path: file.destinationPath, bytes: file.bytes, digest: file.digest }))
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
if (!equal(observed, expected)) throw new Error('Native installed file set or digest mismatch');
|
||||
}
|
||||
|
||||
function parseJson(text, message) {
|
||||
try { return JSON.parse(text); } catch { throw new Error(message); }
|
||||
}
|
||||
|
||||
function componentLine(lines, label) {
|
||||
const match = lines.map(line => line.trim()).map(line => new RegExp(`^${label} \\((\\d+)\\)(?:\\s+(.*))?$`).exec(line)).find(Boolean);
|
||||
if (!match) throw new Error('Native Claude component discovery mismatch');
|
||||
return { count: Number(match[1]), rest: match[2] || '' };
|
||||
}
|
||||
|
||||
// `claude plugin details` has no JSON form; the pinned version fixes this layout.
|
||||
function parsePluginDetails(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const skills = componentLine(lines, 'Skills');
|
||||
const names = skills.count ? skills.rest.split(',').map(name => name.trim()) : [];
|
||||
const counts = ['Agents', 'Hooks', 'MCP servers', 'LSP servers'].map(label => componentLine(lines, label).count);
|
||||
const projection = lines.map(line => /^\s*Always-on:\s+~?(\d{1,3}(?:,\d{3})*|\d+)(k?) tok\b/.exec(line)).find(Boolean);
|
||||
if (!projection) throw new Error('Native Claude token projection is unparseable');
|
||||
const tokens = Number(projection[1].replace(/,/g, '')) * (projection[2] ? 1000 : 1);
|
||||
return { skills: names.length === skills.count ? names.sort() : null, extraComponents: counts.some(count => count !== 0), tokens };
|
||||
}
|
||||
|
||||
function verifyClaude(options, root, carrier, dependencies) {
|
||||
if (command(options, root, ['--version'], dependencies) !== PROVIDERS.claude.versionOutput(options.providerVersion)) {
|
||||
throw new Error('Native Claude version changed since verification');
|
||||
}
|
||||
const pluginDir = path.join(root, 'plugin');
|
||||
const report = parseJson(command(options, root, ['plugin', 'validate', '--json', pluginDir], dependencies), 'Native Claude plugin validation failed');
|
||||
if (report?.success !== true || !Array.isArray(report.manifest?.errors) || report.manifest.errors.length
|
||||
|| !Array.isArray(report.contents) || report.contents.some(item => Array.isArray(item?.errors) && item.errors.length)) {
|
||||
throw new Error('Native Claude plugin validation failed');
|
||||
}
|
||||
const listed = parseJson(command(options, root, ['--plugin-dir', pluginDir, 'plugin', 'list', '--json'], dependencies),
|
||||
'Native Claude plugin discovery mismatch');
|
||||
if (!Array.isArray(listed) || listed.length !== 1 || listed[0]?.id !== `${PLUGIN}@inline` || listed[0].scope !== 'session'
|
||||
|| listed[0].enabled !== true || listed[0].installPath !== pluginDir) throw new Error('Native Claude plugin discovery mismatch');
|
||||
const details = parsePluginDetails(command(options, root, ['--plugin-dir', pluginDir, 'plugin', 'details', PLUGIN], dependencies));
|
||||
if (details.extraComponents || !equal(details.skills, carrier.entries.map(entry => entry.name).sort())) {
|
||||
throw new Error('Native Claude component discovery mismatch');
|
||||
}
|
||||
verifyFiles(pluginDir, carrier);
|
||||
return { surface: 'plugin-always-on-listing', method: `claude-plugin-details@${options.providerVersion}`,
|
||||
tokens: details.tokens, approximate: true };
|
||||
}
|
||||
|
||||
function verifyNative(options, root, carrier, dependencies) {
|
||||
if (options.target === 'claude') return verifyClaude(options, root, carrier, dependencies);
|
||||
if (command(options, root, ['--version'], dependencies) !== `codex-cli ${options.providerVersion}`) throw new Error('Native Codex version changed since verification');
|
||||
const env = environment(root); const marketplaceName = `ecc-context-${carrier.carrierDigest.slice(0, 16)}`;
|
||||
const env = environment(root, 'codex'); const marketplaceName = `ecc-context-${carrier.carrierDigest.slice(0, 16)}`;
|
||||
const cache = path.join(env.CODEX_HOME, 'plugins/cache', marketplaceName, 'ecc-context-carrier/local');
|
||||
const result = (dependencies.discover || discoverSync)(options.codexPath, { cwd: path.join(root, 'project'), env });
|
||||
const result = (dependencies.discover || discoverSync)(options.executablePath, { cwd: path.join(root, 'project'), env });
|
||||
if (!result || !Array.isArray(result.data) || result.data.length !== 1 || !equal(result.data[0].errors, [])
|
||||
|| result.data[0].cwd !== path.join(root, 'project')
|
||||
|| !Array.isArray(result.data[0].skills)) throw new Error('Native skill discovery shape, project binding or parser errors');
|
||||
@@ -249,10 +346,8 @@ function verifyNative(options, root, carrier, dependencies) {
|
||||
const name = skill.name.slice('ecc-context-carrier:'.length);
|
||||
if (!skill.enabled || skill.path !== path.join(cache, 'skills', name, 'SKILL.md')) throw new Error('Native skill discovery enabled state or path mismatch');
|
||||
}
|
||||
const observed = io.inventory(cache).files.sort((a, b) => a.path.localeCompare(b.path));
|
||||
const expected = carrier.files.map(file => ({ path: file.destinationPath, bytes: file.bytes, digest: file.digest }))
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
if (!equal(observed, expected)) throw new Error('Native installed file set or digest mismatch');
|
||||
verifyFiles(cache, carrier);
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkpoint(dependencies, point) { if (dependencies.onCheckpoint) dependencies.onCheckpoint(point); }
|
||||
@@ -296,19 +391,38 @@ function publish(options, before, current, generationId, receipt, dependencies)
|
||||
return response(options, state, current);
|
||||
}
|
||||
|
||||
function register(options, root, carrier, current, dependencies) {
|
||||
for (const relative of ['home', 'home/.codex', 'project', 'marketplace', 'marketplace/.agents', 'marketplace/.agents/plugins', 'marketplace/carrier']) {
|
||||
io.mkdir(path.join(root, relative));
|
||||
}
|
||||
const version = command(options, root, ['--version'], dependencies);
|
||||
const providerVersion = SUPPORTED_VERSIONS.find(value => version === `codex-cli ${value}`);
|
||||
if (!providerVersion) throw new Error(`Native Codex version must be exactly ${SUPPORTED_VERSIONS.join(' or ')}`);
|
||||
function copyCarrier(root, destination, carrier, current) {
|
||||
for (const file of carrier.files) {
|
||||
const relative = `marketplace/carrier/${file.destinationPath}`;
|
||||
const relative = `${destination}/${file.destinationPath}`;
|
||||
const bytes = io.read(path.join(current.generationRoot, file.destinationPath));
|
||||
if (io.hash(bytes) !== file.digest || bytes.length !== file.bytes) throw new Error('Managed carrier source digest changed');
|
||||
io.ensureParents(root, relative); io.writeExclusive(path.join(root, relative), bytes);
|
||||
}
|
||||
}
|
||||
|
||||
function providerVersionFor(options, root, dependencies) {
|
||||
const provider = providerFor(options.target);
|
||||
const version = command(options, root, ['--version'], dependencies);
|
||||
const providerVersion = provider.versions.find(value => version === provider.versionOutput(value));
|
||||
if (!providerVersion) throw new Error(`Native ${provider.label} version must be exactly ${provider.versions.join(' or ')}`);
|
||||
return providerVersion;
|
||||
}
|
||||
|
||||
function registerClaude(options, root, carrier, current, dependencies) {
|
||||
for (const relative of ['home', 'home/.claude', 'project', 'plugin']) io.mkdir(path.join(root, relative));
|
||||
const providerVersion = providerVersionFor(options, root, dependencies);
|
||||
copyCarrier(root, 'plugin', carrier, current);
|
||||
checkpoint(dependencies, 'registered');
|
||||
return { providerVersion, nativeObservation: verifyNative({ ...options, providerVersion }, root, carrier, dependencies) };
|
||||
}
|
||||
|
||||
function register(options, root, carrier, current, dependencies) {
|
||||
if (options.target === 'claude') return registerClaude(options, root, carrier, current, dependencies);
|
||||
for (const relative of ['home', 'home/.codex', 'project', 'marketplace', 'marketplace/.agents', 'marketplace/.agents/plugins', 'marketplace/carrier']) {
|
||||
io.mkdir(path.join(root, relative));
|
||||
}
|
||||
const providerVersion = providerVersionFor(options, root, dependencies);
|
||||
copyCarrier(root, 'marketplace/carrier', carrier, current);
|
||||
const name = `ecc-context-${carrier.carrierDigest.slice(0, 16)}`;
|
||||
io.writeExclusive(path.join(root, 'marketplace/.agents/plugins/marketplace.json'), io.jsonBytes({ name,
|
||||
plugins: [{ name: 'ecc-context-carrier', source: { source: 'local', path: './carrier' },
|
||||
@@ -317,22 +431,23 @@ function register(options, root, carrier, current, dependencies) {
|
||||
command(options, root, ['plugin', 'add', `ecc-context-carrier@${name}`, '--json'], dependencies);
|
||||
checkpoint(dependencies, 'registered');
|
||||
verifyNative({ ...options, providerVersion }, root, carrier, dependencies);
|
||||
return providerVersion;
|
||||
return { providerVersion, nativeObservation: null };
|
||||
}
|
||||
|
||||
function prepareNativeProfile(input, dependencies = {}) {
|
||||
let options = inputs(input); const current = currentStore(options);
|
||||
previewNativeProfile(options);
|
||||
const executable = resolveExecutable(options.codexPath);
|
||||
const provider = providerFor(current.target);
|
||||
const executable = resolveExecutable(options[provider.executableKey], provider.command);
|
||||
owner(options, true);
|
||||
options = { ...options, codexPath: executable.path, executableBinding: executable };
|
||||
options = { ...options, target: current.target, executablePath: executable.path, executableBinding: executable };
|
||||
return locked(options, false, () => {
|
||||
if (exists(path.join(options.nativeRoot, 'pending.json'))) throw new Error('Native attempt requires recovery');
|
||||
const before = readState(options);
|
||||
if (options.expectedRevision !== undefined && options.expectedRevision !== (before?.revision || 0)) throw new Error('Native revision changed since preview');
|
||||
const previous = before ? loadReceipt(options, before, { allowRefresh: true }) : null;
|
||||
const bootstrap = require('./context-profile-interactive').bootstrapFor(options, current);
|
||||
if (before && before.storeRevision === current.revision) {
|
||||
if (before && before.storeRevision === current.revision && previous.target === current.target) {
|
||||
if (previous.receipt.carrierDigest === current.carrierDigest && equal(previous.receipt.executable, executable) && equal(previous.receipt.bootstrap, bootstrap.binding)) {
|
||||
verifyNative({ ...options, providerVersion: previous.receipt.providerVersion }, previous.root, previous.carrier, dependencies);
|
||||
recheckStore(options, current);
|
||||
@@ -350,11 +465,13 @@ function prepareNativeProfile(input, dependencies = {}) {
|
||||
const { carrierDigest, ...body } = carrier;
|
||||
if (carrierDigest !== current.carrierDigest || digestObject(body) !== carrierDigest) throw new Error('Managed carrier descriptor changed before native registration');
|
||||
io.writeExclusive(path.join(root, 'carrier.json'), io.jsonBytes(carrier));
|
||||
const providerVersion = register(options, root, carrier, current, dependencies);
|
||||
io.writeExclusive(path.join(root, 'home/.codex/AGENTS.md'), bootstrap.bytes);
|
||||
const { providerVersion, nativeObservation } = register(options, root, carrier, current, dependencies);
|
||||
io.writeExclusive(path.join(root, provider.bootstrapPath), bootstrap.bytes);
|
||||
const receipt = { schemaVersion: 'ecc.native-context-receipt.v1', generationId,
|
||||
bindingDigest: digestObject({ nativeRoot: options.nativeRoot, stateRoot: options.stateRoot }),
|
||||
carrierDigest: carrier.carrierDigest, providerVersion, executable, bootstrap: bootstrap.binding, controls: snapshot(root) };
|
||||
...(current.target === 'codex' ? {} : { target: current.target }),
|
||||
carrierDigest: carrier.carrierDigest, providerVersion, executable, bootstrap: bootstrap.binding,
|
||||
...(nativeObservation ? { nativeObservation } : {}), controls: snapshot(root, provider) };
|
||||
io.writeExclusive(path.join(root, 'receipt.json'), io.jsonBytes(receipt));
|
||||
checkpoint(dependencies, 'verified');
|
||||
return publish(options, before, current, generationId, receipt, dependencies);
|
||||
@@ -373,8 +490,11 @@ function rollbackNativeProfile(input, dependencies = {}) {
|
||||
const receipt = io.readJson(path.join(root, 'receipt.json'));
|
||||
const previous = loadReceipt(options, { generationId: before.previousGenerationId,
|
||||
generationReceiptDigest: before.previousGenerationReceiptDigest });
|
||||
if (receipt.carrierDigest !== current.carrierDigest) throw new Error('Rollback the managed store to the previous native carrier first');
|
||||
verifyNative({ ...options, providerVersion: receipt.providerVersion, codexPath: receipt.executable.path, executableBinding: receipt.executable }, root, previous.carrier, dependencies);
|
||||
if (receipt.carrierDigest !== current.carrierDigest || previous.target !== current.target) {
|
||||
throw new Error('Rollback the managed store to the previous native carrier first');
|
||||
}
|
||||
verifyNative({ ...options, target: previous.target, providerVersion: receipt.providerVersion,
|
||||
executablePath: receipt.executable.path, executableBinding: receipt.executable }, root, previous.carrier, dependencies);
|
||||
io.atomicJson(path.join(options.nativeRoot, 'pending.json'), { schemaVersion: 'ecc.native-context-pending.v1',
|
||||
before, generationId: before.previousGenerationId, carrierDigest: current.carrierDigest, storeRevision: current.revision });
|
||||
return publish(options, before, current, before.previousGenerationId, receipt, dependencies);
|
||||
@@ -400,4 +520,5 @@ function recoverNativeProfile(input) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getNativeProfileStatus, prepareNativeProfile, previewNativeProfile, recoverNativeProfile, rollbackNativeProfile };
|
||||
module.exports = { CLAUDE_ISOLATION_ENV, getNativeProfileStatus, prepareNativeProfile, previewNativeProfile,
|
||||
recoverNativeProfile, rollbackNativeProfile };
|
||||
+4
-3
@@ -30,7 +30,7 @@ Experimental managed profiles and bounded task context:
|
||||
ecc profile resolve [lean|full] --task-input task.json|- [--load] [--previous receipt.json] [--json]
|
||||
ecc profile run --task-input task.json|- [--state-root <directory>] [--target codex|claude] [--dry-run] [--json]
|
||||
ecc profile set lean|full --state-root <dedicated-directory> [--selection auto|manual|suggest]
|
||||
[--target codex] [--include skill:<id>] [--exclude skill:<id>] [--dry-run] [--json]
|
||||
[--target codex|claude] [--include skill:<id>] [--exclude skill:<id>] [--dry-run] [--json]
|
||||
ecc profile status --state-root <directory> [--json]
|
||||
ecc profile mode auto|manual|suggest --state-root <directory> [--dry-run] [--json]
|
||||
ecc profile rollback --state-root <directory> [--expected-revision N] [--json]
|
||||
@@ -42,11 +42,12 @@ Experimental managed profiles and bounded task context:
|
||||
ecc profile native-recover --state-root <directory> --native-root <directory> [--json]
|
||||
Task input may be one bounded UTF-8 JSON object on stdin with --task-input - (65536 bytes maximum).
|
||||
Start requires prepare-native, launches the pinned native TUI with inherited stdio and provider permissions,
|
||||
and reads a receipt-bound isolated AGENTS bootstrap. Authenticate separately in the isolated home; credentials are never copied.
|
||||
and reads a receipt-bound isolated bootstrap (Codex AGENTS.md, Claude CLAUDE.md). Claude loads the carrier with --plugin-dir;
|
||||
nothing is installed. Authenticate separately in the isolated home; credentials are never copied.
|
||||
Set stages owned generations; provider discovery is verified separately.
|
||||
Resolve returns context only with --load; suggest and --dry-run never return skill bodies.
|
||||
Use resolve --state-root <directory> to honor the saved base, mode and exclusions.
|
||||
Run with --native-root to use a verified isolated Codex generation. Existing sessions are unchanged.
|
||||
Run with --native-root to use a verified isolated Codex or Claude generation. Existing sessions are unchanged.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user