@phnx-labs/agents-cli 1.22.52 → 1.22.54
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 +336 -0
- package/README.md +42 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +220 -174
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +26 -17
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +4 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +8 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +94 -40
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +53 -14
- package/dist/commands/status.js +14 -0
- package/dist/commands/sync.js +44 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +165 -53
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/daemon.js +30 -4
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +8 -7
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +88 -30
- package/dist/lib/exec.js +138 -34
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +35 -2
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.js +8 -1
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +8 -5
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +168 -31
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +126 -37
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +25 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +594 -64
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +24 -0
- package/dist/lib/state.js +318 -54
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +2 -1
package/dist/lib/memory.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import { getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ensureAgentsDir, } from './state.js';
|
|
16
|
+
import { getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, ensureAgentsDir, getRuntimeStateDir, } from './state.js';
|
|
17
17
|
import { agentConfigDirName } from './agents.js';
|
|
18
18
|
import { supports } from './capabilities.js';
|
|
19
|
+
import { claudeProjectDirName } from './project-key.js';
|
|
19
20
|
/** User-layer memory root (~/.agents/memory/). */
|
|
20
21
|
export function getUserMemoryDir() {
|
|
21
22
|
return path.join(getUserAgentsDir(), 'memory');
|
|
@@ -273,3 +274,81 @@ export function syncMemoryToVersionHome(agent, versionHome, cwd = process.cwd())
|
|
|
273
274
|
catch { /* best-effort */ }
|
|
274
275
|
return written;
|
|
275
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Canonical shared dir for Claude Code's NATIVE per-project auto-memory —
|
|
279
|
+
* `<versionHome>/.claude/projects/<project-key>/memory/*.md`, the freeform
|
|
280
|
+
* notes Claude writes for itself during a session. Distinct from the layered
|
|
281
|
+
* `memory` resource above (~/.agents/memory/ facts synced into
|
|
282
|
+
* `.claude/memory/`): this dir is keyed by project (via
|
|
283
|
+
* {@link claudeProjectDirName}), not by agent version, and Claude Code itself
|
|
284
|
+
* decides what goes in it — agents-cli only makes the directory
|
|
285
|
+
* version-independent, never writes into it.
|
|
286
|
+
*/
|
|
287
|
+
export function getClaudeProjectMemoryDir(cwd) {
|
|
288
|
+
const projectKey = claudeProjectDirName(path.resolve(cwd));
|
|
289
|
+
return path.join(getRuntimeStateDir(), 'claude-project-memory', projectKey);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Make Claude Code's native per-project memory dir version-independent by
|
|
293
|
+
* symlinking `<versionHome>/.claude/projects/<project-key>/memory/` into the
|
|
294
|
+
* one canonical dir every installed Claude version's home shares for this
|
|
295
|
+
* project (PHNX-2817). Without this, `getVersionHomePath` gives every
|
|
296
|
+
* installed version its own isolated HOME, so a note written under one
|
|
297
|
+
* version is invisible under another — the directory is just empty there.
|
|
298
|
+
*
|
|
299
|
+
* Idempotent and safe to call on every sync: a dir already linked to the
|
|
300
|
+
* canonical target is left alone; a PRE-EXISTING real directory with content
|
|
301
|
+
* (the common case today, since this bug has always left one behind) has its
|
|
302
|
+
* files migrated into the canonical dir first — never discarded — before
|
|
303
|
+
* being replaced by the symlink.
|
|
304
|
+
*/
|
|
305
|
+
export function syncClaudeProjectMemoryDir(versionHome, cwd = process.cwd()) {
|
|
306
|
+
const projectKey = claudeProjectDirName(path.resolve(cwd));
|
|
307
|
+
const canonicalDir = path.join(getRuntimeStateDir(), 'claude-project-memory', projectKey);
|
|
308
|
+
const projectDir = path.join(versionHome, agentConfigDirName('claude'), 'projects', projectKey);
|
|
309
|
+
const nativeMemoryDir = path.join(projectDir, 'memory');
|
|
310
|
+
fs.mkdirSync(canonicalDir, { recursive: true, mode: 0o700 });
|
|
311
|
+
let existing;
|
|
312
|
+
try {
|
|
313
|
+
existing = fs.lstatSync(nativeMemoryDir);
|
|
314
|
+
}
|
|
315
|
+
catch { /* nothing there yet */ }
|
|
316
|
+
if (existing?.isSymbolicLink()) {
|
|
317
|
+
let currentTarget;
|
|
318
|
+
try {
|
|
319
|
+
currentTarget = fs.readlinkSync(nativeMemoryDir);
|
|
320
|
+
}
|
|
321
|
+
catch { /* dangling link */ }
|
|
322
|
+
if (currentTarget === canonicalDir)
|
|
323
|
+
return; // already wired correctly
|
|
324
|
+
fs.unlinkSync(nativeMemoryDir); // stale/foreign link — replace below
|
|
325
|
+
}
|
|
326
|
+
else if (existing?.isDirectory()) {
|
|
327
|
+
// Migrate first (never clobber content already promoted to canonical by
|
|
328
|
+
// an earlier-synced version home), then remove the now-redundant copy.
|
|
329
|
+
fs.cpSync(nativeMemoryDir, canonicalDir, { recursive: true, force: false, errorOnExist: false });
|
|
330
|
+
fs.rmSync(nativeMemoryDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
else if (existing) {
|
|
333
|
+
return; // an unexpected file at this path — leave it alone rather than destroy it
|
|
334
|
+
}
|
|
335
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
336
|
+
try {
|
|
337
|
+
fs.symlinkSync(canonicalDir, nativeMemoryDir, process.platform === 'win32' ? 'junction' : undefined);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
// A concurrent sync (e.g. two `agents run claude` launches racing on a
|
|
341
|
+
// first-ever project) can win this exact link between our lstat above
|
|
342
|
+
// and this call. If it landed the same canonical target, that's the
|
|
343
|
+
// outcome we wanted — treat it as success rather than throwing.
|
|
344
|
+
if (err?.code !== 'EEXIST')
|
|
345
|
+
throw err;
|
|
346
|
+
let racedTarget;
|
|
347
|
+
try {
|
|
348
|
+
racedTarget = fs.readlinkSync(nativeMemoryDir);
|
|
349
|
+
}
|
|
350
|
+
catch { /* not even a symlink — fall through to rethrow */ }
|
|
351
|
+
if (racedTarget !== canonicalDir)
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -84,6 +84,17 @@ export interface ActionConfig {
|
|
|
84
84
|
notifyChannel?: string;
|
|
85
85
|
/** webhook-out: URL to POST the event to. */
|
|
86
86
|
url?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Shell command that must exit 0 after a `run`/`routine` action settles.
|
|
89
|
+
* Asserts the stated effect actually happened (PHNX-2842) — e.g.
|
|
90
|
+
* `gh pr view 1682 --json state --jq .state | grep -qx MERGED`. `{event}` is
|
|
91
|
+
* replaced with the fired event summary, same as the prompt. Evaluated once
|
|
92
|
+
* the dispatched run is no longer `running`; a failed check makes the fire
|
|
93
|
+
* `ok: false` with effect `none` (`postcondition not met`), not a healthy
|
|
94
|
+
* `completed`. Notify/webhook-out already have a synchronous ok and refuse
|
|
95
|
+
* this field.
|
|
96
|
+
*/
|
|
97
|
+
postcondition?: string;
|
|
87
98
|
}
|
|
88
99
|
/** Full monitor configuration (persisted as YAML in ~/.agents/monitors/). */
|
|
89
100
|
export interface MonitorConfig {
|
|
@@ -238,6 +238,14 @@ export function validateMonitor(config) {
|
|
|
238
238
|
errors.push(`action.url must be an absolute URL (got ${JSON.stringify(action.url)})`);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
if (action.postcondition !== undefined) {
|
|
242
|
+
if (action.type !== 'run' && action.type !== 'routine') {
|
|
243
|
+
errors.push("action.postcondition only applies to run or routine actions");
|
|
244
|
+
}
|
|
245
|
+
else if (typeof action.postcondition !== 'string' || action.postcondition.trim() === '') {
|
|
246
|
+
errors.push('action.postcondition must be a non-empty shell command');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
241
249
|
}
|
|
242
250
|
// ─── PLACEMENT ───────────────────────────────────────────────────────────────
|
|
243
251
|
if (config.device !== undefined && config.devices !== undefined) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { listMonitors, monitorRunsOnThisDevice, parseInterval, setMonitorEnabled, } from './config.js';
|
|
15
15
|
import { evaluateSource } from './sources/index.js';
|
|
16
16
|
import { hasChanged, readState, writeState, recordFireTime, writeFireRecord, recordCheck, markDroughtNotified, } from './state.js';
|
|
17
|
-
import { dispatchAction } from './dispatch.js';
|
|
17
|
+
import { dispatchAction, injectEvent } from './dispatch.js';
|
|
18
18
|
import { sendToOwner } from '../notify.js';
|
|
19
19
|
import { readRunMeta } from '../scheduling/routines.js';
|
|
20
20
|
/** How often the engine wakes to check which monitors are due. */
|
|
@@ -282,12 +282,19 @@ export class MonitorEngine {
|
|
|
282
282
|
// exactly the fires whose `ok` needs revisiting; `resolveFireOutcome`
|
|
283
283
|
// (state.ts) never trusts this field — it re-reads the run fresh instead.
|
|
284
284
|
const runStatusAtFire = result.runId ? readRunMeta(monitor.name, result.runId)?.status : undefined;
|
|
285
|
+
// Snapshot the postcondition with `{event}` already interpolated so a later
|
|
286
|
+
// `resolveFireOutcome` can assert the stated effect without re-reading YAML
|
|
287
|
+
// (PHNX-2842). Notify/webhook-out have no run to settle, so they skip this.
|
|
288
|
+
const postcondition = (result.kind === 'run' || result.kind === 'routine') && monitor.action.postcondition
|
|
289
|
+
? injectEvent(monitor.action.postcondition, event)
|
|
290
|
+
: undefined;
|
|
285
291
|
writeFireRecord(event, {
|
|
286
292
|
...(result.runId ? { runId: result.runId } : {}),
|
|
287
293
|
action: result.kind,
|
|
288
294
|
ok: result.ok,
|
|
289
295
|
...(result.error ? { error: result.error } : {}),
|
|
290
296
|
...(runStatusAtFire ? { runStatusAtFire } : {}),
|
|
297
|
+
...(postcondition ? { postcondition } : {}),
|
|
291
298
|
});
|
|
292
299
|
writeState(monitor.name, decision.value, decision.dedupeKey, { lastFiredAt: event.firedAt, fireTimes });
|
|
293
300
|
this.logFn(result.ok ? 'INFO' : 'ERROR', `monitor '${monitor.name}' fired → ${result.kind}` +
|
|
@@ -118,6 +118,16 @@ export interface FireRecord extends MonitorEvent {
|
|
|
118
118
|
* run fresh on every call instead of trusting this snapshot.
|
|
119
119
|
*/
|
|
120
120
|
runStatusAtFire?: RunMeta['status'];
|
|
121
|
+
/**
|
|
122
|
+
* The action's postcondition command, snapshotted at fire time with `{event}`
|
|
123
|
+
* already interpolated (PHNX-2842). `resolveFireOutcome` runs this once the
|
|
124
|
+
* dispatched run has settled `completed`.
|
|
125
|
+
*/
|
|
126
|
+
postcondition?: string;
|
|
127
|
+
/** Result of the postcondition check, persisted after the first evaluation. */
|
|
128
|
+
postconditionOk?: boolean;
|
|
129
|
+
/** stderr/stdout snippet when `postconditionOk` is false. */
|
|
130
|
+
postconditionError?: string;
|
|
121
131
|
}
|
|
122
132
|
/** List a monitor's fire history, chronologically ascending. */
|
|
123
133
|
export declare function listFires(name: string): FireRecord[];
|
|
@@ -127,10 +137,28 @@ export interface ReconciledFireOutcome {
|
|
|
127
137
|
ok: boolean;
|
|
128
138
|
/** The run's live terminal status, when a runId is present and resolvable. */
|
|
129
139
|
runStatus?: RunMeta['status'];
|
|
140
|
+
/**
|
|
141
|
+
* Present when a `completed` run had a postcondition to assert (PHNX-2842).
|
|
142
|
+
* `met` = the command exited 0; `none` = ran but the intended effect did not
|
|
143
|
+
* happen (the fire must not read as `ok`).
|
|
144
|
+
*/
|
|
145
|
+
effect?: 'met' | 'none';
|
|
146
|
+
/** Why the fire is not ok, when the postcondition failed. */
|
|
147
|
+
error?: string;
|
|
130
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Run a fire's postcondition command. Exit 0 means the intended effect happened;
|
|
151
|
+
* anything else (nonzero, timeout, spawn error, empty command) is "no effect".
|
|
152
|
+
* Real `/bin/sh -c` (or `cmd /c`) — the same seam command sources use.
|
|
153
|
+
*/
|
|
154
|
+
export declare function evaluatePostcondition(command: string): {
|
|
155
|
+
ok: boolean;
|
|
156
|
+
error?: string;
|
|
157
|
+
};
|
|
131
158
|
/**
|
|
132
159
|
* Reconcile a fire's frozen `ok` against its dispatched run's REAL, current
|
|
133
|
-
* status — the render-time fix for RUSH-2690
|
|
160
|
+
* status — the render-time fix for RUSH-2690 — and, when the run has settled
|
|
161
|
+
* `completed`, against a declared postcondition (PHNX-2842).
|
|
134
162
|
*
|
|
135
163
|
* `writeFireRecord` (this module) persists `ok` once, at fire time, from
|
|
136
164
|
* `dispatchAction`'s synchronous return. For a `run`/`routine` action that
|
|
@@ -146,5 +174,13 @@ export interface ReconciledFireOutcome {
|
|
|
146
174
|
* A fire with no `runId` (a `notify`/`webhook-out` action, or a `run`/`routine`
|
|
147
175
|
* dispatch that never got a runId at all) has nothing to reconcile against —
|
|
148
176
|
* its frozen `ok` is the only signal and is returned as-is.
|
|
177
|
+
*
|
|
178
|
+
* `completed` is not success by itself. An agent that exits 0 without doing
|
|
179
|
+
* the job (merge-on-green that never merged) used to record `ok` because
|
|
180
|
+
* `OK_RUN_STATUSES` treated `completed` as healthy. When the fire carries a
|
|
181
|
+
* `postcondition` command, this function runs it once the run has settled and
|
|
182
|
+
* returns `ok: false, effect: 'none'` when it fails — distinguishing "ran but
|
|
183
|
+
* no effect" from a working fire. The result is persisted on the fire record
|
|
184
|
+
* so later listings do not re-exec the command.
|
|
149
185
|
*/
|
|
150
186
|
export declare function resolveFireOutcome(jobName: string, fire: FireRecord): ReconciledFireOutcome;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
16
17
|
import { getMonitorsHistoryDir, ensureAgentsDir } from '../state.js';
|
|
17
18
|
import { safeJoin } from '../paths.js';
|
|
18
19
|
import { readRunMeta } from '../scheduling/routines.js';
|
|
@@ -199,11 +200,50 @@ export function listFires(name) {
|
|
|
199
200
|
}
|
|
200
201
|
return fires;
|
|
201
202
|
}
|
|
202
|
-
/**
|
|
203
|
-
const
|
|
203
|
+
/** Cap a display-time postcondition so `monitors runs` cannot hang on a stuck command. */
|
|
204
|
+
const POSTCONDITION_TIMEOUT_MS = 15_000;
|
|
205
|
+
/**
|
|
206
|
+
* Run a fire's postcondition command. Exit 0 means the intended effect happened;
|
|
207
|
+
* anything else (nonzero, timeout, spawn error, empty command) is "no effect".
|
|
208
|
+
* Real `/bin/sh -c` (or `cmd /c`) — the same seam command sources use.
|
|
209
|
+
*/
|
|
210
|
+
export function evaluatePostcondition(command) {
|
|
211
|
+
const trimmed = command.trim();
|
|
212
|
+
if (!trimmed)
|
|
213
|
+
return { ok: false, error: 'postcondition not met: empty command' };
|
|
214
|
+
const [bin, args] = process.platform === 'win32'
|
|
215
|
+
? ['cmd', ['/c', trimmed]]
|
|
216
|
+
: ['/bin/sh', ['-c', trimmed]];
|
|
217
|
+
const result = spawnSync(bin, args, {
|
|
218
|
+
encoding: 'utf-8',
|
|
219
|
+
timeout: POSTCONDITION_TIMEOUT_MS,
|
|
220
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
221
|
+
env: { ...process.env, CLICOLOR: '0', NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
222
|
+
});
|
|
223
|
+
if (result.status === 0)
|
|
224
|
+
return { ok: true };
|
|
225
|
+
if (result.error) {
|
|
226
|
+
const err = result.error;
|
|
227
|
+
if (err.code === 'ETIMEDOUT') {
|
|
228
|
+
return { ok: false, error: `postcondition not met: timed out after ${POSTCONDITION_TIMEOUT_MS / 1000}s` };
|
|
229
|
+
}
|
|
230
|
+
return { ok: false, error: `postcondition not met: ${err.message}` };
|
|
231
|
+
}
|
|
232
|
+
const detail = (result.stderr || result.stdout || '').trim().replace(/\s+/g, ' ').slice(0, 200);
|
|
233
|
+
const exit = result.status ?? 'unknown';
|
|
234
|
+
return { ok: false, error: `postcondition not met${detail ? `: ${detail}` : ` (exit ${exit})`}` };
|
|
235
|
+
}
|
|
236
|
+
/** Persist the postcondition result onto the existing fire record. Frozen `ok` is left as the fire-time snapshot. */
|
|
237
|
+
function persistPostcondition(fire, result) {
|
|
238
|
+
writeFireRecord(fire, {
|
|
239
|
+
postconditionOk: result.ok,
|
|
240
|
+
...(result.error ? { postconditionError: result.error } : {}),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
204
243
|
/**
|
|
205
244
|
* Reconcile a fire's frozen `ok` against its dispatched run's REAL, current
|
|
206
|
-
* status — the render-time fix for RUSH-2690
|
|
245
|
+
* status — the render-time fix for RUSH-2690 — and, when the run has settled
|
|
246
|
+
* `completed`, against a declared postcondition (PHNX-2842).
|
|
207
247
|
*
|
|
208
248
|
* `writeFireRecord` (this module) persists `ok` once, at fire time, from
|
|
209
249
|
* `dispatchAction`'s synchronous return. For a `run`/`routine` action that
|
|
@@ -219,6 +259,14 @@ const OK_RUN_STATUSES = new Set(['running', 'completed']);
|
|
|
219
259
|
* A fire with no `runId` (a `notify`/`webhook-out` action, or a `run`/`routine`
|
|
220
260
|
* dispatch that never got a runId at all) has nothing to reconcile against —
|
|
221
261
|
* its frozen `ok` is the only signal and is returned as-is.
|
|
262
|
+
*
|
|
263
|
+
* `completed` is not success by itself. An agent that exits 0 without doing
|
|
264
|
+
* the job (merge-on-green that never merged) used to record `ok` because
|
|
265
|
+
* `OK_RUN_STATUSES` treated `completed` as healthy. When the fire carries a
|
|
266
|
+
* `postcondition` command, this function runs it once the run has settled and
|
|
267
|
+
* returns `ok: false, effect: 'none'` when it fails — distinguishing "ran but
|
|
268
|
+
* no effect" from a working fire. The result is persisted on the fire record
|
|
269
|
+
* so later listings do not re-exec the command.
|
|
222
270
|
*/
|
|
223
271
|
export function resolveFireOutcome(jobName, fire) {
|
|
224
272
|
if (!fire.runId)
|
|
@@ -226,5 +274,32 @@ export function resolveFireOutcome(jobName, fire) {
|
|
|
226
274
|
const run = readRunMeta(jobName, fire.runId);
|
|
227
275
|
if (!run)
|
|
228
276
|
return { ok: fire.ok !== false };
|
|
229
|
-
|
|
277
|
+
if (run.status === 'running')
|
|
278
|
+
return { ok: true, runStatus: run.status };
|
|
279
|
+
if (run.status !== 'completed')
|
|
280
|
+
return { ok: false, runStatus: run.status };
|
|
281
|
+
if (!fire.postcondition)
|
|
282
|
+
return { ok: true, runStatus: 'completed' };
|
|
283
|
+
if (fire.postconditionOk === true) {
|
|
284
|
+
return { ok: true, runStatus: 'completed', effect: 'met' };
|
|
285
|
+
}
|
|
286
|
+
if (fire.postconditionOk === false) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
runStatus: 'completed',
|
|
290
|
+
effect: 'none',
|
|
291
|
+
...(fire.postconditionError ? { error: fire.postconditionError } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const result = evaluatePostcondition(fire.postcondition);
|
|
295
|
+
persistPostcondition(fire, result);
|
|
296
|
+
fire.postconditionOk = result.ok;
|
|
297
|
+
if (result.error)
|
|
298
|
+
fire.postconditionError = result.error;
|
|
299
|
+
return {
|
|
300
|
+
ok: result.ok,
|
|
301
|
+
runStatus: 'completed',
|
|
302
|
+
effect: result.ok ? 'met' : 'none',
|
|
303
|
+
...(result.error ? { error: result.error } : {}),
|
|
304
|
+
};
|
|
230
305
|
}
|
|
@@ -7,6 +7,8 @@ export declare const KIRO_CAPABILITY_BY_TOOL: Record<string, string | undefined>
|
|
|
7
7
|
export declare const CANONICAL_TO_OPENCLAW_TOOL: Record<string, string>;
|
|
8
8
|
/** Canonical tool -> Antigravity's action namespace. */
|
|
9
9
|
export declare const ANTIGRAVITY_ACTION_BY_TOOL: Record<string, string | undefined>;
|
|
10
|
+
/** Filename Codex stores generated deny rules under `.codex/rules/`. */
|
|
11
|
+
export declare const CODEX_RULES_FILENAME = "agents-deny.rules";
|
|
10
12
|
/**
|
|
11
13
|
* Strip JSON comments for JSONC parsing, only OUTSIDE string literals.
|
|
12
14
|
*
|
|
@@ -97,6 +97,8 @@ const CANONICAL_BY_GROK_TOOL = invertFirstWins(GROK_TOOL_BY_CANONICAL);
|
|
|
97
97
|
const CANONICAL_BY_KIRO_CAPABILITY = invertFirstWins(KIRO_CAPABILITY_BY_TOOL);
|
|
98
98
|
const CANONICAL_BY_OPENCLAW_TOOL = invertFirstWins(CANONICAL_TO_OPENCLAW_TOOL);
|
|
99
99
|
const CANONICAL_BY_ANTIGRAVITY_ACTION = invertFirstWins(ANTIGRAVITY_ACTION_BY_TOOL);
|
|
100
|
+
/** Filename Codex stores generated deny rules under `.codex/rules/`. */
|
|
101
|
+
export const CODEX_RULES_FILENAME = 'agents-deny.rules';
|
|
100
102
|
/** Canonical TitleCase spelling for a lowercased canonical tool name. */
|
|
101
103
|
const CANONICAL_TOOL_CASE = {
|
|
102
104
|
bash: 'Bash',
|
|
@@ -248,6 +250,106 @@ function existingOr(preferred, alternate) {
|
|
|
248
250
|
function stringList(value) {
|
|
249
251
|
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
250
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Invert `convertDenyToCodexRules` (permissions.ts). The writer emits
|
|
255
|
+
* `prefix_rule(pattern=["git", "reset"], decision="forbidden")` from a
|
|
256
|
+
* canonical `Bash(git reset:*)`. Prefix-match is the native form of `:*`,
|
|
257
|
+
* so the reverse always reconstructs `Bash(<parts>:*)`.
|
|
258
|
+
*
|
|
259
|
+
* Only `decision = "forbidden"` is a deny; anything else is skipped rather
|
|
260
|
+
* than guessed into an allow (Codex grants come from sandbox_mode).
|
|
261
|
+
*/
|
|
262
|
+
function convertCodexRulesToDeny(content) {
|
|
263
|
+
const deny = [];
|
|
264
|
+
const marker = 'prefix_rule';
|
|
265
|
+
let searchFrom = 0;
|
|
266
|
+
while (searchFrom < content.length) {
|
|
267
|
+
const start = content.indexOf(marker, searchFrom);
|
|
268
|
+
if (start < 0)
|
|
269
|
+
break;
|
|
270
|
+
const open = content.indexOf('(', start + marker.length);
|
|
271
|
+
if (open < 0)
|
|
272
|
+
break;
|
|
273
|
+
const bodyEnd = findMatchingCloser(content, open, '(', ')');
|
|
274
|
+
if (bodyEnd < 0) {
|
|
275
|
+
searchFrom = open + 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const body = content.slice(open + 1, bodyEnd);
|
|
279
|
+
searchFrom = bodyEnd + 1;
|
|
280
|
+
const decision = body.match(/decision\s*=\s*"((?:\\.|[^"\\])*)"/);
|
|
281
|
+
if (!decision || decision[1] !== 'forbidden')
|
|
282
|
+
continue;
|
|
283
|
+
const patternKw = body.search(/pattern\s*=/);
|
|
284
|
+
if (patternKw < 0)
|
|
285
|
+
continue;
|
|
286
|
+
const arrayStart = body.indexOf('[', patternKw);
|
|
287
|
+
if (arrayStart < 0)
|
|
288
|
+
continue;
|
|
289
|
+
const arrayEnd = findMatchingCloser(body, arrayStart, '[', ']');
|
|
290
|
+
if (arrayEnd < 0)
|
|
291
|
+
continue;
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(body.slice(arrayStart, arrayEnd + 1));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(parsed))
|
|
300
|
+
continue;
|
|
301
|
+
const parts = parsed.filter((p) => typeof p === 'string' && p.length > 0);
|
|
302
|
+
if (parts.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
deny.push(`Bash(${parts.join(' ')}:*)`);
|
|
305
|
+
}
|
|
306
|
+
return deny;
|
|
307
|
+
}
|
|
308
|
+
/** Index of the closer matching `source[openIdx]`, skipping quoted strings. */
|
|
309
|
+
function findMatchingCloser(source, openIdx, open, close) {
|
|
310
|
+
let depth = 0;
|
|
311
|
+
let inString = false;
|
|
312
|
+
let escape = false;
|
|
313
|
+
for (let i = openIdx; i < source.length; i++) {
|
|
314
|
+
const c = source[i];
|
|
315
|
+
if (inString) {
|
|
316
|
+
if (escape) {
|
|
317
|
+
escape = false;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (c === '\\') {
|
|
321
|
+
escape = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (c === '"')
|
|
325
|
+
inString = false;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (c === '"') {
|
|
329
|
+
inString = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (c === open)
|
|
333
|
+
depth++;
|
|
334
|
+
else if (c === close) {
|
|
335
|
+
depth--;
|
|
336
|
+
if (depth === 0)
|
|
337
|
+
return i;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return -1;
|
|
341
|
+
}
|
|
342
|
+
function readCodexDenyRules(configPath) {
|
|
343
|
+
const rulesPath = path.join(path.dirname(configPath), 'rules', CODEX_RULES_FILENAME);
|
|
344
|
+
if (!fs.existsSync(rulesPath))
|
|
345
|
+
return [];
|
|
346
|
+
try {
|
|
347
|
+
return convertCodexRulesToDeny(fs.readFileSync(rulesPath, 'utf-8'));
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
251
353
|
/**
|
|
252
354
|
* Single source of truth for where each allowlist-capable agent stores
|
|
253
355
|
* permissions and how to read them back. Keys MUST equal
|
|
@@ -309,24 +411,24 @@ export const PERMISSION_TARGETS = {
|
|
|
309
411
|
codex: {
|
|
310
412
|
home: (h) => path.join(h, '.codex', 'config.toml'),
|
|
311
413
|
project: (cwd) => path.join(cwd, '.codex', 'config.toml'),
|
|
312
|
-
lossyBecause: '
|
|
414
|
+
lossyBecause: 'sandbox mode widens into representative blanket grants; deny round-trips from agents-deny.rules as Bash(prefix:*)',
|
|
313
415
|
toCanonical(configPath) {
|
|
314
416
|
const config = readToml(configPath);
|
|
315
|
-
if (!config)
|
|
316
|
-
return null;
|
|
317
417
|
const allow = [];
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
418
|
+
if (config) {
|
|
419
|
+
const sandboxMode = config.sandbox_mode;
|
|
420
|
+
if (config.approval_policy === 'never' || sandboxMode === 'danger-full-access') {
|
|
421
|
+
allow.push('Bash(*)', 'Read(**)', 'Write(**)', 'Edit(**)');
|
|
422
|
+
}
|
|
423
|
+
else if (sandboxMode === 'workspace-write') {
|
|
424
|
+
allow.push('Bash(*)', 'Read(**)');
|
|
425
|
+
}
|
|
426
|
+
const sw = config.sandbox_workspace_write;
|
|
427
|
+
if (sw && typeof sw === 'object' && !Array.isArray(sw) && sw.network_access) {
|
|
428
|
+
allow.push('WebSearch(*)', 'WebFetch(*)');
|
|
429
|
+
}
|
|
328
430
|
}
|
|
329
|
-
return permissionSet(allow,
|
|
431
|
+
return permissionSet(allow, readCodexDenyRules(configPath));
|
|
330
432
|
},
|
|
331
433
|
},
|
|
332
434
|
cursor: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId, PermissionSet, InstalledPermission, ClaudePermissions, CursorPermissions, OpenCodePermissions, CodexPermissions } from './types.js';
|
|
2
|
-
|
|
3
|
-
export
|
|
2
|
+
import { CODEX_RULES_FILENAME } from './permissions-registry.js';
|
|
3
|
+
export { CODEX_RULES_FILENAME };
|
|
4
4
|
export type ParsedRules = PermissionSet;
|
|
5
5
|
export declare const COMPUTER_PERMISSION_RULE_PREFIX = "Computer";
|
|
6
6
|
export declare const COMPUTER_APP_GATED_VERBS: readonly ["screenshot", "describe", "get-text", "launch", "raise", "click", "right-click", "type", "type-text", "key", "drag", "scroll", "ax-action", "focus", "wait"];
|
|
@@ -13,6 +13,9 @@ export declare function containsBroadGrants(rules: ParsedRules): {
|
|
|
13
13
|
/**
|
|
14
14
|
* Convert canonical deny rules to Codex Starlark .rules format.
|
|
15
15
|
* E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
|
|
16
|
+
*
|
|
17
|
+
* Inverse: `toCanonical` for `PERMISSION_TARGETS.codex` reads
|
|
18
|
+
* `.codex/rules/agents-deny.rules` back into `Bash(<parts>:*)`.
|
|
16
19
|
*/
|
|
17
20
|
export declare function convertDenyToCodexRules(deny: string[]): string | null;
|
|
18
21
|
/**
|
|
@@ -344,4 +347,3 @@ export declare function saveDefaultPermissionSet(set: PermissionSet): {
|
|
|
344
347
|
success: boolean;
|
|
345
348
|
error?: string;
|
|
346
349
|
};
|
|
347
|
-
export {};
|
package/dist/lib/permissions.js
CHANGED
|
@@ -20,16 +20,9 @@ import { updateGeminiSettings } from './gemini-settings.js';
|
|
|
20
20
|
// The canonical<->native tool vocabularies live in the registry, which also owns
|
|
21
21
|
// the reverse projections — so the serializers below and the readers there can
|
|
22
22
|
// never disagree about what `fs_read` or `developer__shell` means.
|
|
23
|
-
import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, stripJsonComments, GROK_TOOL_BY_CANONICAL, KIRO_CAPABILITY_BY_TOOL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
|
|
23
|
+
import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL, KIRO_CAPABILITY_BY_TOOL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
|
|
24
|
+
export { CODEX_RULES_FILENAME };
|
|
24
25
|
const HOME = os.homedir();
|
|
25
|
-
// PERMISSIONS_CAPABLE_AGENTS removed — use `capableAgents('allowlist')`
|
|
26
|
-
// from lib/capabilities.ts. The capability matrix on AgentConfig is the
|
|
27
|
-
// single source of truth. (Per-agent native format details:
|
|
28
|
-
// antigravity → ~/.gemini/antigravity-cli/settings.json `permissions.{allow,deny}`
|
|
29
|
-
// grok → ~/.grok/config.toml `[permission].rules`
|
|
30
|
-
// the writer in `applyPermissionsToVersion` handles the format dispatch.)
|
|
31
|
-
/** Filename used for Codex Starlark deny-rules generated from permission groups. */
|
|
32
|
-
export const CODEX_RULES_FILENAME = 'agents-deny.rules';
|
|
33
26
|
export const COMPUTER_PERMISSION_RULE_PREFIX = 'Computer';
|
|
34
27
|
export const COMPUTER_APP_GATED_VERBS = [
|
|
35
28
|
'screenshot',
|
|
@@ -103,6 +96,9 @@ export function containsBroadGrants(rules) {
|
|
|
103
96
|
/**
|
|
104
97
|
* Convert canonical deny rules to Codex Starlark .rules format.
|
|
105
98
|
* E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
|
|
99
|
+
*
|
|
100
|
+
* Inverse: `toCanonical` for `PERMISSION_TARGETS.codex` reads
|
|
101
|
+
* `.codex/rules/agents-deny.rules` back into `Bash(<parts>:*)`.
|
|
106
102
|
*/
|
|
107
103
|
export function convertDenyToCodexRules(deny) {
|
|
108
104
|
const rules = [];
|
|
@@ -122,6 +118,24 @@ export function convertDenyToCodexRules(deny) {
|
|
|
122
118
|
return null;
|
|
123
119
|
return `# Auto-generated by agents-cli from deny permission groups.\n# Do not edit manually — re-run "agents use" to regenerate.\n\n${rules.join('\n\n')}\n`;
|
|
124
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Write `.codex/rules/agents-deny.rules` to match `deny`, or delete it when
|
|
123
|
+
* the current set has nothing to forbid. The reader (PHNX-2703) now surfaces
|
|
124
|
+
* this file, so a later apply with an empty deny must not leave a stale
|
|
125
|
+
* forbid behind — `agents permissions list codex` would keep reporting it.
|
|
126
|
+
*/
|
|
127
|
+
function syncCodexDenyRules(configDir, deny) {
|
|
128
|
+
const rulesPath = path.join(configDir, 'rules', CODEX_RULES_FILENAME);
|
|
129
|
+
const rulesContent = deny && deny.length > 0 ? convertDenyToCodexRules(deny) : null;
|
|
130
|
+
if (rulesContent) {
|
|
131
|
+
fs.mkdirSync(path.dirname(rulesPath), { recursive: true });
|
|
132
|
+
fs.writeFileSync(rulesPath, rulesContent, 'utf-8');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (fs.existsSync(rulesPath)) {
|
|
136
|
+
fs.unlinkSync(rulesPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
125
139
|
/**
|
|
126
140
|
* Ensure central permissions directory exists.
|
|
127
141
|
*/
|
|
@@ -1274,15 +1288,7 @@ function applyCodexPermissions(set, scope = 'user', cwd, merge = true) {
|
|
|
1274
1288
|
: newPermissions.sandbox_workspace_write;
|
|
1275
1289
|
}
|
|
1276
1290
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
1277
|
-
|
|
1278
|
-
if (set.deny && set.deny.length > 0) {
|
|
1279
|
-
const rulesContent = convertDenyToCodexRules(set.deny);
|
|
1280
|
-
if (rulesContent) {
|
|
1281
|
-
const rulesDir = path.join(configDir, 'rules');
|
|
1282
|
-
fs.mkdirSync(rulesDir, { recursive: true });
|
|
1283
|
-
fs.writeFileSync(path.join(rulesDir, CODEX_RULES_FILENAME), rulesContent, 'utf-8');
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1291
|
+
syncCodexDenyRules(configDir, set.deny);
|
|
1286
1292
|
return { success: true };
|
|
1287
1293
|
}
|
|
1288
1294
|
catch (err) {
|
|
@@ -1393,15 +1399,7 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1393
1399
|
: newPermissions.sandbox_workspace_write;
|
|
1394
1400
|
}
|
|
1395
1401
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
1396
|
-
|
|
1397
|
-
if (set.deny && set.deny.length > 0) {
|
|
1398
|
-
const rulesContent = convertDenyToCodexRules(set.deny);
|
|
1399
|
-
if (rulesContent) {
|
|
1400
|
-
const rulesDir = path.join(configDir, 'rules');
|
|
1401
|
-
fs.mkdirSync(rulesDir, { recursive: true });
|
|
1402
|
-
fs.writeFileSync(path.join(rulesDir, CODEX_RULES_FILENAME), rulesContent, 'utf-8');
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1402
|
+
syncCodexDenyRules(configDir, set.deny);
|
|
1405
1403
|
return { success: true };
|
|
1406
1404
|
}
|
|
1407
1405
|
if (agentId === 'antigravity') {
|
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -248,13 +248,14 @@ export interface ResolvedProfileRun {
|
|
|
248
248
|
*/
|
|
249
249
|
tierNote?: string;
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
251
|
+
* Concrete model id callers should forward as `ExecOptions.model`. Set when:
|
|
252
|
+
* - `requestedModel` was a cost-tier token resolved against this profile's
|
|
253
|
+
* `models:` map, OR
|
|
254
|
+
* - no `--model` was requested and this is an OpenCode host whose pin lives
|
|
255
|
+
* in `OPENCODE_MODEL` (OpenCode does not read that env var, so the pin
|
|
256
|
+
* has to become `--model` on the argv).
|
|
257
|
+
* Undefined when the caller already passed a concrete `--model`, or when
|
|
258
|
+
* tier resolution degraded (see `tierNote`).
|
|
258
259
|
*/
|
|
259
260
|
resolvedModel?: string;
|
|
260
261
|
}
|
package/dist/lib/profiles.js
CHANGED
|
@@ -164,6 +164,7 @@ const MODEL_ENV_KEYS = [
|
|
|
164
164
|
'OPENAI_MODEL',
|
|
165
165
|
'GEMINI_MODEL',
|
|
166
166
|
'GROK_MODEL',
|
|
167
|
+
'OPENCODE_MODEL',
|
|
167
168
|
];
|
|
168
169
|
/** Return the configured model env value for display. */
|
|
169
170
|
export function profileModelLabel(profile) {
|
|
@@ -583,6 +584,17 @@ export function resolveProfileForRun(name, requestedModel) {
|
|
|
583
584
|
// the raw tier token downstream and handles the message -- this function
|
|
584
585
|
// doesn't compete with that canonical fallback for the no-opt-in case.
|
|
585
586
|
}
|
|
587
|
+
// OpenCode does not honor OPENCODE_MODEL (unlike claude/codex/gemini/grok,
|
|
588
|
+
// whose host CLIs read their MODEL env var). Copy the pin into resolvedModel
|
|
589
|
+
// so callers set ExecOptions.model and buildExecCommand emits `--model`.
|
|
590
|
+
// An explicit `--model` (including a cost-tier token the caller may still
|
|
591
|
+
// discard) wins and is left alone.
|
|
592
|
+
if (resolved.resolvedModel === undefined && !requestedModel && profile.host.agent === 'opencode') {
|
|
593
|
+
const envKey = profileModelEnvKey(profile) ?? modelEnvKeyForHost('opencode');
|
|
594
|
+
const pinned = env[envKey];
|
|
595
|
+
if (pinned)
|
|
596
|
+
resolved.resolvedModel = pinned;
|
|
597
|
+
}
|
|
586
598
|
return resolved;
|
|
587
599
|
}
|
|
588
600
|
/**
|