aptly 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aptly)](https://www.npmjs.com/package/aptly)
7
7
  [![MIT license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/abruption/aptly/blob/main/LICENSE)
8
8
 
9
- **Model and reasoning recommendations for subagents.**
9
+ **Model and reasoning recommendations for subagents, powered by the OpenAI Decisions API.**
10
10
 
11
11
  [한국어](README.md) · **English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
12
12
 
@@ -20,15 +20,17 @@ Our own tests showed **about 52–58% lower API-equivalent costs**. See the [tes
20
20
 
21
21
  ## Quick start
22
22
 
23
- Requires Node.js **24 or later** and Claude Code or Codex. Use the commands below **after 0.1.1 is published on npm**.
23
+ Requires Node.js **24 or later** and Claude Code or Codex. Install the latest stable version globally with these commands.
24
24
 
25
25
  ```sh
26
- npm install -g aptly@0.1.1
26
+ npm install -g aptly@latest
27
27
  aptly setup --client all --mode defaults
28
28
  aptly --version
29
29
  ```
30
30
 
31
- The version command should print `0.1.1`.
31
+ Check the installed version with `aptly --version`.
32
+
33
+ Before upgrading, stop all related host, MCP, and CLI processes and review the ledger transition in the [installation and upgrade guide](docs/guide/en.md#installation).
32
34
 
33
35
  During installation, `postinstall` registers the MCP server and a `PreToolUse` Hook in existing host configuration directories. Run `setup` explicitly if automatic registration was skipped or a host has no configuration directory yet. Other settings and Hooks, and any existing Aptly launch command and environment, are preserved.
34
36
 
package/README.ja.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aptly)](https://www.npmjs.com/package/aptly)
7
7
  [![MIT license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/abruption/aptly/blob/main/LICENSE)
8
8
 
9
- **Model and reasoning recommendations for subagents.**
9
+ **Model and reasoning recommendations for subagents, powered by the OpenAI Decisions API.**
10
10
 
11
11
  [한국어](README.md) · [English](README.en.md) · [简体中文](README.zh-CN.md) · **日本語**
12
12
 
@@ -20,15 +20,17 @@ Aptly は必要最小限のタスク要約を OpenAI Decisions API で分類し
20
20
 
21
21
  ## クイックスタート
22
22
 
23
- Node.js **24 以上**と Claude Code または Codex が必要です。以下のコマンドは **0.1.1 が npm で公開された後**に使用してください。
23
+ Node.js **24 以上**と Claude Code または Codex が必要です。以下のコマンドで最新の安定版をグローバルインストールしてください。
24
24
 
25
25
  ```sh
26
- npm install -g aptly@0.1.1
26
+ npm install -g aptly@latest
27
27
  aptly setup --client all --mode defaults
28
28
  aptly --version
29
29
  ```
30
30
 
31
- バージョン確認コマンドの出力が `0.1.1` であることを確認してください。
31
+ `aptly --version` でインストール済みのバージョンを確認してください。
32
+
33
+ アップグレード前に、関連するホスト・MCP・CLI の全プロセスを停止し、[インストールとアップグレードの案内](docs/guide/ja.md#インストール)で台帳の移行手順を確認してください。
32
34
 
33
35
  インストール時の `postinstall` は、既存のホスト設定ディレクトリに MCP と `PreToolUse` Hook を登録します。自動登録をスキップした場合や、ホストに設定ディレクトリがまだない場合も、`setup` を明示的に実行して登録できます。他の既存設定・Hook と、既存の Aptly の実行コマンド・環境は保持します。
34
36
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aptly)](https://www.npmjs.com/package/aptly)
7
7
  [![MIT license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/abruption/aptly/blob/main/LICENSE)
8
8
 
9
- **Model and reasoning recommendations for subagents.**
9
+ **Model and reasoning recommendations for subagents, powered by the OpenAI Decisions API.**
10
10
 
11
11
  **한국어** · [English](README.en.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
12
12
 
@@ -20,15 +20,17 @@ Aptly는 최소한의 작업 요약을 OpenAI Decisions API로 분류하고 허
20
20
 
21
21
  ## 빠른 시작
22
22
 
23
- Node.js **24 이상**과 Claude Code 또는 Codex가 필요합니다. 아래 명령은 **0.1.1이 npm에 게시된 후** 사용하십시오.
23
+ Node.js **24 이상**과 Claude Code 또는 Codex가 필요합니다. 다음 명령으로 최신 안정 버전을 전역 설치하십시오.
24
24
 
25
25
  ```sh
26
- npm install -g aptly@0.1.1
26
+ npm install -g aptly@latest
27
27
  aptly setup --client all --mode defaults
28
28
  aptly --version
29
29
  ```
30
30
 
31
- 버전 확인 명령의 출력은 `0.1.1`입니다.
31
+ `aptly --version`으로 설치된 버전을 확인하십시오.
32
+
33
+ 업그레이드 전에는 관련 호스트·MCP·CLI 프로세스를 모두 중지하고 [설치·업그레이드 안내](docs/guide/ko.md#설치)의 원장 전환 절차를 확인하십시오.
32
34
 
33
35
  설치 시 `postinstall`은 기존 호스트 설정 디렉터리에 MCP와 `PreToolUse` Hook을 등록합니다. 명시적인 `setup`은 자동 등록을 건너뛰었거나 아직 설정 디렉터리가 없는 호스트에도 사용할 수 있습니다. 기존 다른 설정·Hook과 기존 Aptly 실행 명령·환경은 보존합니다.
34
36
 
package/README.zh-CN.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/aptly)](https://www.npmjs.com/package/aptly)
7
7
  [![MIT license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/abruption/aptly/blob/main/LICENSE)
8
8
 
9
- **Model and reasoning recommendations for subagents.**
9
+ **Model and reasoning recommendations for subagents, powered by the OpenAI Decisions API.**
10
10
 
11
11
  [한국어](README.md) · [English](README.en.md) · **简体中文** · [日本語](README.ja.md)
12
12
 
@@ -20,15 +20,17 @@ Aptly 使用 OpenAI Decisions API 对简要任务摘要进行分类,并推荐
20
20
 
21
21
  ## 快速开始
22
22
 
23
- 需要 Node.js **24 或更高版本**以及 Claude Code 或 Codex。请在 **0.1.1 发布到 npm 后**使用以下命令。
23
+ 需要 Node.js **24 或更高版本**以及 Claude Code 或 Codex。请使用以下命令全局安装最新稳定版。
24
24
 
25
25
  ```sh
26
- npm install -g aptly@0.1.1
26
+ npm install -g aptly@latest
27
27
  aptly setup --client all --mode defaults
28
28
  aptly --version
29
29
  ```
30
30
 
31
- 版本检查命令应输出 `0.1.1`。
31
+ 请使用 `aptly --version` 确认已安装的版本。
32
+
33
+ 升级前,请停止所有相关宿主、MCP 和 CLI 进程,并查看[安装与升级指南](docs/guide/zh-CN.md#安装)中的账本迁移步骤。
32
34
 
33
35
  安装时的 `postinstall` 会在现有宿主配置目录中注册 MCP 和 `PreToolUse` Hook。如果跳过了自动注册,或宿主尚无配置目录,也可以显式运行 `setup`。其他现有配置与 Hook,以及 Aptly 原有的启动命令和环境均会保留。
34
36
 
package/dist/bin/aptly.js CHANGED
@@ -9,9 +9,9 @@ import { collectDiagnostics } from '../src/diagnostics.js';
9
9
  import { exportDiagnosticReport } from '../src/report.js';
10
10
  import { fetchModelInfo, saveModelInfo } from '../src/model-info.js';
11
11
  import { errorCode } from '../src/types.js';
12
- import { setupHosts, removeSetup, setDefaults, setEnvironment, configurableEnvironmentKeys } from '../src/setup.js';
12
+ import { setupHosts, removeSetup, setDefaults, setEnvironment, configurableEnvironmentKeys, migrateLauncher } from '../src/setup.js';
13
13
  import { readEnvironmentValue } from '../src/cli-input.js';
14
- import { setApiKey } from '../src/credential-setup.js';
14
+ import { prepareApiKey } from '../src/credential-setup.js';
15
15
  const [command = 'serve', ...args] = process.argv.slice(2);
16
16
  const output = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
17
17
  try {
@@ -39,7 +39,7 @@ try {
39
39
  }
40
40
  else if (command === 'set') {
41
41
  if (args.length === 1 && args[0] === '--help')
42
- process.stdout.write('Usage: aptly set default [--client codex|claude|all]\n aptly set env ENV_KEY [--client codex|claude|all] [--stdin]\nValues are entered interactively with hidden input; --stdin explicitly reads a value from standard input. Existing defaults and launchers are preserved. OPENAI_API_KEY is stored in the OS credential store; host files contain references only. Restart MCP after changes.\n');
42
+ process.stdout.write('Usage: aptly set default [--client codex|claude|all]\n aptly set env ENV_KEY [--client codex|claude|all] [--stdin | --check]\n--check diagnoses OPENAI_API_KEY registration without reading or storing a key.\nValues are entered interactively with hidden input; --stdin explicitly reads a value from standard input. Existing defaults and launchers are preserved. OPENAI_API_KEY is stored in the OS credential store; host files contain references only. Restart MCP after changes.\n');
43
43
  else {
44
44
  const action = args[0];
45
45
  let key;
@@ -54,12 +54,17 @@ try {
54
54
  throw new Error('set_invalid_options');
55
55
  let clients = ['codex', 'claude'];
56
56
  let fromStdin = false;
57
+ let checkOnly = false;
57
58
  const seen = new Set();
58
59
  for (let i = start; i < args.length; i++) {
59
60
  const flag = args[i];
60
61
  if (seen.has(flag))
61
62
  throw new Error('set_invalid_options');
62
63
  seen.add(flag);
64
+ if (flag === '--check' && action === 'env' && key === 'OPENAI_API_KEY') {
65
+ checkOnly = true;
66
+ continue;
67
+ }
63
68
  if (flag === '--stdin' && action === 'env') {
64
69
  fromStdin = true;
65
70
  continue;
@@ -72,33 +77,52 @@ try {
72
77
  throw new Error('set_invalid_options');
73
78
  }
74
79
  const packageRoot = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../' : '../../', import.meta.url));
80
+ if (checkOnly && fromStdin)
81
+ throw new Error('set_invalid_options');
82
+ const credential = key === 'OPENAI_API_KEY' ? prepareApiKey({ clients, packageRoot }) : undefined;
75
83
  let value = '';
76
- try {
77
- if (action === 'env')
78
- value = await readEnvironmentValue({ key: key, fromStdin });
79
- const result = action === 'default' ? setDefaults({ clients, packageRoot }) : key === 'OPENAI_API_KEY' ? setApiKey(value, { clients, packageRoot }) : setEnvironment(key, value, { clients, packageRoot });
80
- output(result);
81
- if (result.status === 'partial')
84
+ if (credential && (checkOnly || credential.result.status === 'partial')) {
85
+ output(credential.result);
86
+ if (credential.result.status === 'partial')
82
87
  process.exitCode = 1;
83
88
  }
84
- finally {
85
- value = '';
86
- }
89
+ else
90
+ try {
91
+ if (action === 'env')
92
+ value = await readEnvironmentValue({ key: key, fromStdin });
93
+ const result = action === 'default' ? setDefaults({ clients, packageRoot }) : key === 'OPENAI_API_KEY' ? credential.apply(value) : setEnvironment(key, value, { clients, packageRoot });
94
+ output(result);
95
+ if (result.status === 'partial')
96
+ process.exitCode = 1;
97
+ }
98
+ finally {
99
+ value = '';
100
+ }
87
101
  }
88
102
  }
89
103
  else if (['setup', 'remove'].includes(command)) {
90
104
  if (args.length === 1 && args[0] === '--help')
91
- process.stdout.write('Usage: aptly setup [--client codex|claude|all] [--mode defaults|advisory] [--automatic]\n aptly remove [--client codex|claude|all]\nSetup merges MCP and PreToolUse registration. Codex requires native /hooks trust review.\nAutomatic setup skips CI, APTLY_SKIP_SETUP=1, and absent host directories. No keys are stored.\n');
105
+ process.stdout.write('Usage: aptly setup [--client codex|claude|all] [--mode defaults|advisory] [--automatic]\n aptly setup --client codex|claude --migrate-launcher [--dry-run]\n aptly remove [--client codex|claude|all]\nSetup merges MCP and PreToolUse registration. Codex requires native /hooks trust review.\nAutomatic setup skips CI, APTLY_SKIP_SETUP=1, and absent host directories. No keys are stored.\n');
92
106
  else {
93
107
  let clients = ['codex', 'claude'];
94
108
  let mode = 'defaults';
95
109
  let automatic = false;
110
+ let migrate = false;
111
+ let dryRun = false;
96
112
  const seen = new Set();
97
113
  for (let i = 0; i < args.length; i++) {
98
114
  const flag = args[i];
99
115
  if (seen.has(flag))
100
116
  throw new Error('setup_invalid_options');
101
117
  seen.add(flag);
118
+ if (flag === '--migrate-launcher' && command === 'setup') {
119
+ migrate = true;
120
+ continue;
121
+ }
122
+ if (flag === '--dry-run' && command === 'setup') {
123
+ dryRun = true;
124
+ continue;
125
+ }
102
126
  if (flag === '--automatic' && command === 'setup') {
103
127
  automatic = true;
104
128
  continue;
@@ -112,7 +136,9 @@ try {
112
136
  throw new Error('setup_invalid_options');
113
137
  }
114
138
  const packageRoot = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? '../' : '../../', import.meta.url));
115
- const result = command === 'setup' ? setupHosts({ clients, mode, automatic, packageRoot }) : removeSetup({ clients });
139
+ if ((migrate && (clients.length !== 1 || automatic || seen.has('--mode'))) || (dryRun && !migrate))
140
+ throw new Error('setup_invalid_options');
141
+ const result = migrate ? migrateLauncher({ clients, packageRoot }, dryRun) : command === 'setup' ? setupHosts({ clients, mode, automatic, packageRoot }) : removeSetup({ clients });
116
142
  output(result);
117
143
  if (result.status === 'partial' && !automatic)
118
144
  process.exitCode = 1;
@@ -23,6 +23,7 @@ export declare class Router {
23
23
  ledgerPath: string | undefined;
24
24
  state: LedgerState;
25
25
  active: number;
26
+ private heldLedgerLock;
26
27
  constructor({ policy, provider, ledgerPath }?: RouterOptions);
27
28
  capabilities(): {
28
29
  policy_version: string;
package/dist/src/core.js CHANGED
@@ -1,7 +1,8 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, lstatSync, chmodSync } from 'node:fs';
3
3
  import { isRecord, errorCode } from './types.js';
4
4
  import { builtInProfiles, resolveCatalog, isSupportedProfile } from './catalog.js';
5
+ import { acquireLedgerLock } from './ledger-lock.js';
5
6
  const defaultCatalog = resolveCatalog({});
6
7
  export const defaultPolicy = Object.freeze({
7
8
  version: 'aptly-v010', catalog_version: defaultCatalog.version, max_summary_bytes: 4000,
@@ -66,6 +67,7 @@ export class Router {
66
67
  ledgerPath;
67
68
  state;
68
69
  active;
70
+ heldLedgerLock;
69
71
  constructor({ policy = defaultPolicy, provider, ledgerPath } = {}) {
70
72
  this.policy = clone(policy);
71
73
  const integerCaps = { max_summary_bytes: 64000, max_concurrency: 16, max_daily_calls: 100000, max_total_calls: 1000000, timeout_ms: 60000 };
@@ -123,6 +125,7 @@ export class Router {
123
125
  load() {
124
126
  if (!this.ledgerPath)
125
127
  return;
128
+ this.heldLedgerLock?.assertHeld();
126
129
  try {
127
130
  const stat = lstatSync(this.ledgerPath);
128
131
  if (!stat.isFile() || stat.isSymbolicLink())
@@ -158,13 +161,15 @@ export class Router {
158
161
  save() {
159
162
  if (!this.ledgerPath)
160
163
  return;
161
- const tmp = `${this.ledgerPath}.write-${process.pid}`;
164
+ this.heldLedgerLock?.assertHeld();
165
+ const tmp = `${this.ledgerPath}.write-${randomUUID()}`;
162
166
  let fd;
163
167
  try {
164
168
  fd = openSync(tmp, 'wx', 0o600);
165
169
  writeFileSync(fd, JSON.stringify(this.state));
166
170
  closeSync(fd);
167
171
  fd = undefined;
172
+ this.heldLedgerLock?.assertHeld();
168
173
  renameSync(tmp, this.ledgerPath);
169
174
  }
170
175
  finally {
@@ -228,12 +233,13 @@ export class Router {
228
233
  let lock;
229
234
  if (this.ledgerPath) {
230
235
  try {
231
- lock = openSync(`${this.ledgerPath}.lock`, 'wx', 0o600);
236
+ lock = acquireLedgerLock(this.ledgerPath);
232
237
  }
233
- catch {
234
- return this.result('unavailable', 'ledger_locked');
238
+ catch (error) {
239
+ return this.result('unavailable', errorCode(error) === 'ledger_locked' ? 'ledger_locked' : 'ledger_error');
235
240
  }
236
241
  }
242
+ this.heldLedgerLock = lock;
237
243
  this.active++;
238
244
  try {
239
245
  this.load();
@@ -308,7 +314,7 @@ export class Router {
308
314
  }
309
315
  catch (e) {
310
316
  // A failed transport can have consumed a paid call. Preserve its unknown marker.
311
- const allowed = ['timeout', 'refused', 'truncated', 'invalid_json', 'unauthorized', 'rate_limited', 'network'];
317
+ const allowed = ['timeout', 'refused', 'truncated', 'invalid_json', 'unauthorized', 'rate_limited', 'network', 'ledger_error'];
312
318
  const code = errorCode(e);
313
319
  return this.result('unavailable', code !== undefined && allowed.includes(code) ? code : 'provider_error', { latency_ms: latency() });
314
320
  }
@@ -321,12 +327,12 @@ export class Router {
321
327
  }
322
328
  finally {
323
329
  this.active--;
330
+ this.heldLedgerLock = undefined;
324
331
  if (lock !== undefined) {
325
- closeSync(lock);
326
332
  try {
327
- unlinkSync(`${this.ledgerPath}.lock`);
333
+ lock.release();
328
334
  }
329
- catch { }
335
+ catch { /* Never unlink a persistent OS-owned gate. */ }
330
336
  }
331
337
  }
332
338
  }
@@ -11,4 +11,8 @@ export interface CredentialBackend {
11
11
  }): void;
12
12
  }
13
13
  /** Key bytes enter the OS store only; host files receive nonsecret references. */
14
+ export declare function prepareApiKey(options?: SetupOptions): {
15
+ result: SetupResult;
16
+ apply(value: string, backend?: CredentialBackend): SetupResult;
17
+ };
14
18
  export declare function setApiKey(value: string, options?: SetupOptions, backend?: CredentialBackend): SetupResult;
@@ -1,7 +1,18 @@
1
1
  import { loadSecret, storeSecret, deleteSecret } from './secret-store.js';
2
- import { setCredentialReference } from './setup.js';
2
+ import { prepareCredentialReference } from './setup.js';
3
3
  /** Restore once, then verify even if the restore's acknowledgement was lost. */
4
- function restore(previous, account, backend) {
4
+ function restore(previous, attemptedValue, account, backend) {
5
+ // Do not overwrite an observed concurrent credential change or an unreadable value.
6
+ try {
7
+ const current = backend.loadSecret({ account });
8
+ if (current === previous)
9
+ return true;
10
+ if (current !== attemptedValue)
11
+ return false;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
5
16
  try {
6
17
  if (previous === null)
7
18
  backend.deleteSecret({ account });
@@ -17,40 +28,62 @@ function restore(previous, account, backend) {
17
28
  }
18
29
  }
19
30
  /** Key bytes enter the OS store only; host files receive nonsecret references. */
20
- export function setApiKey(value, options = {}, backend = { loadSecret, storeSecret, deleteSecret }) {
31
+ export function prepareApiKey(options = {}) {
21
32
  const clients = [...new Set(options.clients ?? ['codex', 'claude'])];
22
33
  if (clients.some(c => c !== 'codex' && c !== 'claude'))
23
- return { status: 'partial', clients: [] };
24
- if (!/^sk-[A-Za-z0-9_-]{12,}$/.test(value) || Buffer.byteLength(value) > 8192)
25
- return { status: 'partial', clients: clients.map(client => ({ client, status: 'error', codes: ['credential_value_invalid'] })) };
26
- const results = [];
27
- for (const client of clients) {
28
- const account = `${client}:OPENAI_API_KEY`;
29
- let previous = null, attempted = false;
30
- try {
31
- previous = backend.loadSecret({ account });
32
- // A timeout/process/output error may occur after the OS committed a write.
33
- attempted = true;
34
- backend.storeSecret(value, { account });
35
- const configured = setCredentialReference({ ...options, clients: [client] });
36
- const result = configured.clients[0];
37
- if (!result || result.status === 'error' || result.status === 'skipped') {
38
- const recovered = restore(previous, account, backend);
39
- const failed = result ?? { client, status: 'error', codes: ['credential_configuration_failed'] };
40
- results.push(recovered ? failed : { ...failed, status: 'error', codes: [...failed.codes, 'credential_rollback_failed'] });
34
+ throw new Error('set_invalid_options');
35
+ const plans = clients.map(client => prepareCredentialReference(options, client));
36
+ const result = { status: plans.some(p => p.result.status === 'error') ? 'partial' : 'complete', clients: plans.map(p => p.result) };
37
+ let used = false;
38
+ return { result, apply(value, backend = { loadSecret, storeSecret, deleteSecret }) {
39
+ if (used)
40
+ return { status: 'partial', clients: clients.map(client => ({ client, status: 'error', codes: ['credential_plan_consumed'] })) };
41
+ used = true;
42
+ if (!/^sk-[A-Za-z0-9_-]{12,}$/.test(value) || Buffer.byteLength(value) > 8192)
43
+ return { status: 'partial', clients: clients.map(client => ({ client, status: 'error', codes: ['credential_value_invalid'] })) };
44
+ const results = [];
45
+ for (const plan of plans) {
46
+ const client = plan.result.client;
47
+ const checked = plan.check();
48
+ if (checked.status === 'error') {
49
+ results.push(checked);
50
+ continue;
51
+ }
52
+ const account = `${client}:OPENAI_API_KEY`;
53
+ let previous = null, attempted = false;
54
+ try {
55
+ previous = backend.loadSecret({ account });
56
+ const rechecked = plan.check();
57
+ if (rechecked.status === 'error') {
58
+ results.push(rechecked);
59
+ continue;
60
+ }
61
+ // A timeout/process/output error may occur after the OS committed a write.
62
+ attempted = true;
63
+ backend.storeSecret(value, { account });
64
+ const result = plan.commit();
65
+ if (!result || result.status === 'error' || result.status === 'skipped') {
66
+ const recovered = restore(previous, value, account, backend);
67
+ const failed = result ?? { client, status: 'error', codes: ['credential_configuration_failed'] };
68
+ results.push(recovered ? failed : { ...failed, status: 'error', codes: [...failed.codes, 'credential_rollback_failed'] });
69
+ }
70
+ else
71
+ results.push({ ...result, codes: [...result.codes, 'credential_stored_in_os', 'mcp_restart_required'] });
72
+ }
73
+ catch (error) {
74
+ const recovered = !attempted || restore(previous, value, account, backend);
75
+ const allowed = ['secure_store_invalid_options', 'secure_store_invalid_value', 'secure_store_unavailable', 'secure_store_timeout', 'secure_store_failed', 'secure_store_cancelled', 'secure_store_denied', 'secure_store_locked', 'secure_store_unsupported_platform'];
76
+ const code = error instanceof Error && allowed.includes(error.message) ? error.message : 'credential_store_failed';
77
+ results.push({ client, status: 'error', codes: [code, ...(recovered ? [] : ['credential_rollback_failed'])] });
78
+ }
79
+ finally {
80
+ previous = null;
81
+ }
41
82
  }
42
- else
43
- results.push({ ...result, codes: [...result.codes, 'credential_stored_in_os', 'mcp_restart_required'] });
83
+ return { status: results.some(r => r.status === 'error') ? 'partial' : 'complete', clients: results };
44
84
  }
45
- catch (error) {
46
- const recovered = !attempted || restore(previous, account, backend);
47
- const allowed = ['secure_store_invalid_options', 'secure_store_invalid_value', 'secure_store_unavailable', 'secure_store_timeout', 'secure_store_failed', 'secure_store_cancelled', 'secure_store_denied', 'secure_store_locked', 'secure_store_unsupported_platform'];
48
- const code = error instanceof Error && allowed.includes(error.message) ? error.message : 'credential_store_failed';
49
- results.push({ client, status: 'error', codes: [code, ...(recovered ? [] : ['credential_rollback_failed'])] });
50
- }
51
- finally {
52
- previous = null;
53
- }
54
- }
55
- return { status: results.some(r => r.status === 'error') ? 'partial' : 'complete', clients: results };
85
+ };
86
+ }
87
+ export function setApiKey(value, options = {}, backend = { loadSecret, storeSecret, deleteSecret }) {
88
+ return prepareApiKey(options).apply(value, backend);
56
89
  }
@@ -0,0 +1,8 @@
1
+ export interface LedgerLock {
2
+ /** lstat-only while held: never open/read/close the canonical SQLite file. */
3
+ assertHeld(): void;
4
+ /** Roll back and close this connection. Never unlink the persistent gate. */
5
+ release(): void;
6
+ }
7
+ /** Local, hard-link-capable filesystems only; no PID or stale-age recovery. */
8
+ export declare function acquireLedgerLock(ledgerPath: string): LedgerLock;
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, linkSync, lstatSync, openSync, unlinkSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ import { assertNoRedirectedAncestors, assertPrivateStatePath } from './files.js';
6
+ const require = createRequire(import.meta.url);
7
+ const applicationId = 0x4150544c;
8
+ const userVersion = 1;
9
+ function fail(code) { throw Object.assign(new Error(code), { code }); }
10
+ const codeOf = (error) => error && typeof error === 'object' && 'code' in error ? error.code : undefined;
11
+ const sqliteCode = (error) => {
12
+ if (!error || typeof error !== 'object' || !('errcode' in error) || typeof error.errcode !== 'number')
13
+ return;
14
+ return error.errcode & 0xff;
15
+ };
16
+ function identity(path, directory = false) {
17
+ const stat = lstatSync(path, { bigint: true });
18
+ if ((directory ? !stat.isDirectory() : !stat.isFile()) || stat.isSymbolicLink() || stat.ino === 0n)
19
+ fail('ledger_error');
20
+ if (process.platform !== 'win32' && ((stat.mode & 63n) !== 0n || (process.getuid && stat.uid !== BigInt(process.getuid()))))
21
+ fail('ledger_error');
22
+ return { dev: stat.dev, ino: stat.ino, uid: stat.uid, mode: stat.mode };
23
+ }
24
+ function same(actual, expected) {
25
+ return actual.dev === expected.dev && actual.ino === expected.ino && actual.uid === expected.uid && actual.mode === expected.mode;
26
+ }
27
+ function close(connection, transaction) {
28
+ if (!connection)
29
+ return true;
30
+ let success = true;
31
+ if (transaction) {
32
+ try {
33
+ connection.exec('ROLLBACK');
34
+ }
35
+ catch {
36
+ success = false;
37
+ }
38
+ }
39
+ try {
40
+ connection.close();
41
+ }
42
+ catch {
43
+ success = false;
44
+ }
45
+ return success;
46
+ }
47
+ /**
48
+ * Publish a completely initialized gate with an atomic no-replace hard link.
49
+ * Existing empty/unknown locks belong to the legacy protocol and are untouched.
50
+ * Never open an old stage alias: a crash after link may leave it sharing the gate
51
+ * inode, and an ordinary fs close could cancel this process's SQLite POSIX lock.
52
+ */
53
+ function initialize(path, Database) {
54
+ const stage = join(dirname(path), `.aptly-lock-init-${randomUUID()}`);
55
+ let created = false, connection, transaction = false;
56
+ try {
57
+ const fd = openSync(stage, 'wx', 0o600);
58
+ created = true;
59
+ closeSync(fd);
60
+ assertPrivateStatePath(stage);
61
+ identity(stage);
62
+ connection = new Database(stage);
63
+ connection.exec('PRAGMA journal_mode=DELETE; PRAGMA busy_timeout=0; BEGIN IMMEDIATE');
64
+ transaction = true;
65
+ connection.exec(`PRAGMA application_id=${applicationId}; PRAGMA user_version=${userVersion}; COMMIT`);
66
+ transaction = false;
67
+ connection.close();
68
+ connection = undefined;
69
+ try {
70
+ linkSync(stage, path);
71
+ }
72
+ catch (error) {
73
+ if (codeOf(error) !== 'EEXIST')
74
+ throw error;
75
+ }
76
+ }
77
+ finally {
78
+ close(connection, transaction);
79
+ // Only our exclusive, unique stage is removed; the canonical gate persists.
80
+ if (created)
81
+ unlinkSync(stage);
82
+ }
83
+ }
84
+ /** Local, hard-link-capable filesystems only; no PID or stale-age recovery. */
85
+ export function acquireLedgerLock(ledgerPath) {
86
+ let connection, transaction = false;
87
+ try {
88
+ const path = `${ledgerPath}.lock`;
89
+ assertPrivateStatePath(ledgerPath);
90
+ assertPrivateStatePath(path);
91
+ const parent = identity(dirname(path), true);
92
+ // Load only for persistent ledgers. Fixture-only callers need no SQLite.
93
+ const { DatabaseSync } = require('node:sqlite');
94
+ try {
95
+ lstatSync(path);
96
+ }
97
+ catch (error) {
98
+ if (codeOf(error) !== 'ENOENT')
99
+ throw error;
100
+ initialize(path, DatabaseSync);
101
+ }
102
+ assertPrivateStatePath(path);
103
+ const before = identity(path);
104
+ // Do not let SQLite initialize an existing empty legacy ownership marker.
105
+ if (lstatSync(path).size < 512)
106
+ fail('ledger_locked');
107
+ connection = new DatabaseSync(path);
108
+ const assertIdentity = () => {
109
+ // ACLs are checked before opening; while held use only path metadata.
110
+ assertNoRedirectedAncestors(dirname(path));
111
+ if (!same(identity(dirname(path), true), parent) || !same(identity(path), before))
112
+ fail('ledger_error');
113
+ };
114
+ assertIdentity();
115
+ connection.exec('PRAGMA busy_timeout=0; BEGIN IMMEDIATE');
116
+ transaction = true;
117
+ assertIdentity();
118
+ const app = connection.prepare('PRAGMA application_id').get();
119
+ const version = connection.prepare('PRAGMA user_version').get();
120
+ const journal = connection.prepare('PRAGMA journal_mode').get();
121
+ if (app?.application_id !== applicationId || version?.user_version !== userVersion || journal?.journal_mode !== 'delete')
122
+ fail('ledger_locked');
123
+ assertIdentity();
124
+ const owned = connection;
125
+ let released = false;
126
+ return {
127
+ assertHeld() {
128
+ if (released)
129
+ fail('ledger_error');
130
+ try {
131
+ assertIdentity();
132
+ }
133
+ catch {
134
+ fail('ledger_error');
135
+ }
136
+ },
137
+ release() {
138
+ if (released)
139
+ return;
140
+ released = true;
141
+ if (!close(owned, true))
142
+ fail('ledger_error');
143
+ },
144
+ };
145
+ }
146
+ catch (error) {
147
+ close(connection, transaction);
148
+ const code = codeOf(error), sqlite = sqliteCode(error);
149
+ if (code === 'ledger_locked' || sqlite === 5 || sqlite === 6 || sqlite === 11 || sqlite === 26)
150
+ fail('ledger_locked');
151
+ fail('ledger_error');
152
+ }
153
+ }
@@ -9,7 +9,13 @@ export function decisionsProvider({ key, fetchImpl = fetch, descriptions = {} }
9
9
  preflight();
10
10
  const payload = {
11
11
  model: 'gpt-6-luna',
12
- input: JSON.stringify({ task: input.task, client: input.client }),
12
+ // Keep the wire boundary explicit even for internal callers that bypass
13
+ // Router validation. Unrelated task/native metadata is never serialized.
14
+ input: JSON.stringify({ task: {
15
+ summary: input.task.summary,
16
+ operation: input.task.operation,
17
+ context_sufficient: input.task.context_sufficient,
18
+ }, client: input.client }),
13
19
  questions: [{ type: 'choice', name: 'profile', instructions, choices: [
14
20
  ...profiles.map(p => ({ value: p.id, description: descriptions[p.id] ?? `${p.model}${p.reasoning_effort ? ' / effort ' + p.reasoning_effort : ''}. Select only for an appropriate task; capabilities not supplied.` })),
15
21
  { value: 'keep_current', description: 'Preserve the existing selection; no confident supported choice.' },