@aiwg/cli 2026.10.6 → 2026.10.7

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.
Files changed (84) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/decision.js +3 -2
  18. package/dist/src/cli/handlers/effect.js +4 -84
  19. package/dist/src/cli/handlers/guided-setup.js +1 -1
  20. package/dist/src/cli/handlers/init.js +1 -1
  21. package/dist/src/cli/handlers/install.js +6 -0
  22. package/dist/src/cli/handlers/marketplace.js +15 -4
  23. package/dist/src/cli/handlers/setup.js +24 -20
  24. package/dist/src/cli/handlers/use.js +57 -15
  25. package/dist/src/cli/prompt-utils.js +73 -0
  26. package/dist/src/config/gitignore.js +4 -0
  27. package/dist/src/config/workspace.js +2 -1
  28. package/dist/src/decision/adapters/jev.js +20 -31
  29. package/dist/src/decision/ask.js +6 -1
  30. package/dist/src/decision/benchmark.js +19 -6
  31. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  32. package/dist/src/decision/qualification/load-staged.js +47 -10
  33. package/dist/src/decision/qualification/runner.js +7 -3
  34. package/dist/src/discovery/interview.js +10 -5
  35. package/dist/src/discovery/plan-apply.js +26 -30
  36. package/dist/src/discovery/plan-validation.js +2 -2
  37. package/dist/src/effects/host.js +91 -0
  38. package/dist/src/extensions/commands/definitions.js +2 -2
  39. package/dist/src/extensions/project-local-gitignore.js +2 -0
  40. package/dist/src/issues/cli.js +16 -5
  41. package/dist/src/issues/live.js +44 -12
  42. package/dist/src/marketplace/exchange.js +18 -6
  43. package/dist/src/marketplace/provenance.js +3 -2
  44. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  45. package/dist/src/mcp/cli.mjs +17 -90
  46. package/dist/src/mcp/grok-build-config.mjs +2 -1
  47. package/dist/src/mcp/muse-native-config.mjs +2 -1
  48. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  49. package/dist/src/memory/project-registry.js +2 -1
  50. package/dist/src/mission-protocol/worker-status.js +91 -1
  51. package/dist/src/models/worker-installation.d.mts +1 -1
  52. package/dist/src/models/worker-installation.mjs +65 -4
  53. package/dist/src/packages/adapters/git.js +99 -23
  54. package/dist/src/packages/adapters/gitea.js +8 -27
  55. package/dist/src/packages/adapters/github.js +7 -25
  56. package/dist/src/packages/registry.js +4 -4
  57. package/dist/src/process/cli-command.d.mts +5 -0
  58. package/dist/src/process/cli-command.mjs +42 -0
  59. package/dist/src/serve/worker-invocation.js +10 -0
  60. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  61. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  62. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  63. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  64. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  65. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  66. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  67. package/dist/src/tracker/project-inputs.js +2 -0
  68. package/dist/src/update/notifier.mjs +2 -4
  69. package/package.json +1 -1
  70. package/tools/agents/providers/base.mjs +5 -1
  71. package/tools/agents/providers/claude.mjs +5 -2
  72. package/tools/agents/providers/codex.mjs +4 -1
  73. package/tools/agents/providers/context-write.mjs +28 -0
  74. package/tools/agents/providers/copilot.mjs +5 -2
  75. package/tools/agents/providers/hermes.mjs +5 -2
  76. package/tools/agents/providers/omp.mjs +3 -1
  77. package/tools/agents/providers/rule-delivery.mjs +29 -12
  78. package/tools/agents/providers/warp.mjs +4 -1
  79. package/tools/agents/providers/windsurf.mjs +6 -3
  80. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  81. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  82. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  83. package/tools/ralph-external/process-monitor.mjs +42 -4
  84. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -1,5 +1,10 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { createHash } from "node:crypto";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import Ajv2020 from "ajv/dist/2020.js";
6
+ import addFormats from "ajv-formats";
7
+ import { exportShardWithReport, importShard, packTarGz, unpackTarGz, validateChecksums, validateFullV1ShardArchive, verifyShardSignature, } from "@fortemi/core";
3
8
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
4
9
  const SUPPORTED_SHARD_TUPLES = new Set([
5
10
  "1.2.0/core-v1",
@@ -130,4 +135,283 @@ export function diagnoseAiwgFortemiShardMigration(cwd, inputPath) {
130
135
  + "--schema-version 2.0.0 --profile full-v1; retain the legacy shard for rollback.",
131
136
  };
132
137
  }
138
+ /** Qualified reader tuples; this is a local candidate result, not a release receipt. */
139
+ export const AIWG_FULL_V1_SHARD_READER_VERSIONS = ["2.0.0", "2.1.0", "2.2.0"];
140
+ const SHARD_CONTRACT_COMMIT = "b88084407751153e67a9279fff9303cc3db70474";
141
+ const encoder = new TextEncoder();
142
+ const decoder = new TextDecoder();
143
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
144
+ const encodeJson = (value) => encoder.encode(JSON.stringify(value));
145
+ const parseJson = (bytes) => JSON.parse(decoder.decode(bytes));
146
+ const extendedFields = {
147
+ embedding_sets: ["query_template", "vector_source"],
148
+ embedding_configs: ["provider_dimension", "space_contract", "space_id", "vector_type"],
149
+ };
150
+ const contractRoot = new URL("./fortemi-shard-contract/2.2.0/", import.meta.url);
151
+ const contractBytes = fs.readFileSync(new URL("contract.json", contractRoot));
152
+ const contract = JSON.parse(contractBytes.toString("utf8"));
153
+ const contractAjv = new Ajv2020({ strict: true, allErrors: true });
154
+ addFormats(contractAjv);
155
+ const extendedValidators = Object.fromEntries(["manifest", "embedding-set", "embedding-config"].map((name) => [name, contractAjv.compile(JSON.parse(fs.readFileSync(new URL(`full-v1-${name}.schema.json`, contractRoot), "utf8")))]));
156
+ function projectedRecord(component, record) {
157
+ const projected = { ...record };
158
+ for (const field of extendedFields[component] ?? [])
159
+ delete projected[field];
160
+ return projected;
161
+ }
162
+ function projectedFiles(original) {
163
+ const files = new Map(original);
164
+ const manifest = parseJson(files.get("manifest.json"));
165
+ manifest.version = "2.0.0";
166
+ manifest.min_reader_version = "2.0.0";
167
+ for (const component of Object.keys(extendedFields)) {
168
+ const filename = `${component}.json`;
169
+ if (!files.has(filename))
170
+ continue;
171
+ const records = parseJson(files.get(filename));
172
+ const bytes = encodeJson(records.map(record => projectedRecord(component, record)));
173
+ files.set(filename, bytes);
174
+ manifest.checksums[filename] = digest(bytes);
175
+ }
176
+ files.set("manifest.json", encodeJson(manifest));
177
+ // The publisher envelope is verified against the original bytes before import.
178
+ files.delete("signature.json");
179
+ return files;
180
+ }
181
+ /** Validate the candidate additions, then reuse Core's complete structure/reference/blob checks. */
182
+ export async function validateAiwgFortemiKnowledgeShard(archive) {
183
+ try {
184
+ const files = unpackTarGz(archive);
185
+ const manifest = parseJson(files.get("manifest.json"));
186
+ if (manifest.version !== "2.2.0" && manifest.version !== "2.1.0") {
187
+ return validateFullV1ShardArchive(archive);
188
+ }
189
+ if (manifest.profile !== "full-v1") {
190
+ return { valid: false, errors: [`Unsupported ${manifest.version}/${manifest.profile} reader tuple`] };
191
+ }
192
+ const errors = [];
193
+ const manifestValidator = extendedValidators.manifest;
194
+ const candidateManifest = manifest.version === "2.1.0"
195
+ ? { ...manifest, version: "2.2.0", min_reader_version: "2.2.0" } : manifest;
196
+ if (manifest.version === "2.1.0" && manifest.min_reader_version !== "2.1.0") {
197
+ errors.push("manifest.json unsupported min_reader_version");
198
+ }
199
+ if (!manifestValidator(candidateManifest))
200
+ errors.push(`manifest.json ${contractAjv.errorsText(manifestValidator.errors)}`);
201
+ for (const [component, name] of [["embedding_sets", "embedding-set"], ["embedding_configs", "embedding-config"]]) {
202
+ const records = parseJson(files.get(`${component}.json`));
203
+ if (!Array.isArray(records))
204
+ throw new Error(`${component} must be a JSON array`);
205
+ for (const [index, record] of records.entries()) {
206
+ const validate = extendedValidators[name];
207
+ if (!validate(record))
208
+ errors.push(`${component}[${index}] ${contractAjv.errorsText(validate.errors)}`);
209
+ if (typeof record.query_template === "string" && Buffer.byteLength(record.query_template, "utf8") > 2048) {
210
+ errors.push(`${component}[${index}].query_template exceeds 2048 UTF-8 bytes`);
211
+ }
212
+ if (typeof record.provider_dimension === "number" && record.provider_dimension < Number(record.dimension)) {
213
+ errors.push(`${component}[${index}].provider_dimension is below dimension`);
214
+ }
215
+ if (manifest.version === "2.1.0" && ["query_template", "vector_source", "provider_dimension"].some(field => field in record)) {
216
+ errors.push(`${component}[${index}] requires schema 2.2.0`);
217
+ }
218
+ }
219
+ }
220
+ if (errors.length)
221
+ return { valid: false, errors };
222
+ const checksums = await validateChecksums(manifest.checksums, files);
223
+ if (!checksums.valid)
224
+ return { valid: false, errors: checksums.failures.map(file => `${file} checksum mismatch`) };
225
+ const view = projectedFiles(files);
226
+ if (files.has("signature.json"))
227
+ view.set("signature.json", files.get("signature.json"));
228
+ return validateFullV1ShardArchive(view);
229
+ }
230
+ catch (error) {
231
+ return { valid: false, errors: [error instanceof Error ? error.message : String(error)] };
232
+ }
233
+ }
234
+ /**
235
+ * Core 2026.7.15 owns the snapshot transaction, conflict and blob handling. Its
236
+ * validation view uses 2.0.0; only the existing snapshot-table parameters change.
237
+ * Stored files/records always retain the original authority tuple and fields.
238
+ */
239
+ function snapshotDatabaseView(db, version, originalFiles, originalArchive) {
240
+ const originalManifest = originalFiles.get("manifest.json");
241
+ const projected = originalManifest ? projectedFiles(originalFiles) : undefined;
242
+ const projectedArchiveDigest = projected ? digest(packTarGz(projected)) : undefined;
243
+ const wrap = (executor) => ({
244
+ exec: sql => executor.exec(sql),
245
+ async query(sql, params) {
246
+ if (!sql.includes("knowledge_shard_"))
247
+ return executor.query(sql, params);
248
+ const mapped = params ? [...params] : undefined;
249
+ if (mapped?.[0] === "2.0.0")
250
+ mapped[0] = version;
251
+ if (mapped && sql.includes("INSERT INTO knowledge_shard_snapshot")) {
252
+ mapped[2] = digest(originalArchive);
253
+ mapped[3] = decoder.decode(originalManifest);
254
+ }
255
+ else if (mapped && sql.includes("INSERT INTO knowledge_shard_file")) {
256
+ mapped[3] = originalFiles.get(String(mapped[2]));
257
+ }
258
+ else if (mapped && sql.includes("INSERT INTO knowledge_shard_component_record")) {
259
+ const component = String(mapped[2]);
260
+ if (component in extendedFields) {
261
+ mapped[5] = JSON.stringify(parseJson(originalFiles.get(`${component}.json`))[Number(mapped[3])]);
262
+ }
263
+ }
264
+ const result = await executor.query(sql.replaceAll("schema_version = '2.0.0'", `schema_version = '${version}'`), mapped);
265
+ if (mapped && sql.includes("INSERT INTO knowledge_shard_file") && mapped[2] === "manifest.json" && originalFiles.has("signature.json")) {
266
+ await executor.query(sql, [version, "full-v1", "signature.json", originalFiles.get("signature.json")]);
267
+ }
268
+ const rows = result.rows.map(row => {
269
+ if ("archive_sha256" in row && originalArchive && row.archive_sha256 === digest(originalArchive)) {
270
+ return { ...row, archive_sha256: projectedArchiveDigest };
271
+ }
272
+ if ("path" in row && "bytes" in row) {
273
+ originalFiles.set(String(row.path), new Uint8Array(row.bytes));
274
+ return row;
275
+ }
276
+ return row;
277
+ });
278
+ // Export must validate the same 2.0.0 view while retaining original files.
279
+ if (rows.some(row => "path" in row && "bytes" in row)) {
280
+ const view = projectedFiles(originalFiles);
281
+ return { ...result, rows: rows.filter(row => row.path !== "signature.json").map(row => ({ ...row, bytes: view.get(String(row.path)) })) };
282
+ }
283
+ return { ...result, rows: rows.map(row => {
284
+ if ("manifest_json" in row) {
285
+ const manifest = structuredClone(row.manifest_json);
286
+ manifest.version = "2.0.0";
287
+ manifest.min_reader_version = "2.0.0";
288
+ return { ...row, manifest_json: manifest };
289
+ }
290
+ if ("record_json" in row)
291
+ return { ...row, record_json: projectedRecord(String(row.component), row.record_json) };
292
+ return row;
293
+ }) };
294
+ },
295
+ });
296
+ return { ...wrap(db), transaction: callback => db.transaction(tx => callback(wrap(tx))) };
297
+ }
298
+ function readerCapability(version, base) {
299
+ return {
300
+ ...base,
301
+ requested_profile: "full-v1",
302
+ requested_schema_version: version,
303
+ authority_status: "candidate",
304
+ backend_supported: true,
305
+ portable: true,
306
+ authority: {
307
+ repository: "Fortemi/fortemi",
308
+ commit: SHARD_CONTRACT_COMMIT,
309
+ contract_sha256: digest(contractBytes),
310
+ contract_revision: "23",
311
+ schema_version: version,
312
+ schema_bundle_sha256: digest(encodeJson(contract.schemaBundle.files)),
313
+ },
314
+ advertised_profiles: ["full-v1"],
315
+ };
316
+ }
317
+ export async function importAiwgFortemiKnowledgeShard(db, archive, options) {
318
+ let files;
319
+ let manifest;
320
+ try {
321
+ files = unpackTarGz(archive);
322
+ manifest = parseJson(files.get("manifest.json"));
323
+ }
324
+ catch {
325
+ return importShard(db, archive, options);
326
+ }
327
+ if (manifest.version === "2.2.0" && manifest.profile !== "full-v1") {
328
+ throw new Error(`Unsupported 2.2.0/${manifest.profile} reader tuple`);
329
+ }
330
+ if (manifest.profile !== "full-v1" || !["2.1.0", "2.2.0"].includes(manifest.version)) {
331
+ return importShard(db, archive, options);
332
+ }
333
+ const validation = await validateAiwgFortemiKnowledgeShard(archive);
334
+ if (!validation.valid)
335
+ throw new Error(`Canonical full-v1 validation failed: ${validation.errors.join("; ")}`);
336
+ const policy = options?.verifySignature ?? (options?.trustStore ? "require" : undefined);
337
+ if (policy && policy !== "trusted-local-only") {
338
+ if (!options?.trustStore)
339
+ throw new Error(`verifySignature: '${policy}' requires a trustStore`);
340
+ const verdict = await verifyShardSignature({ files, trustStore: options.trustStore });
341
+ if (!verdict.ok && !(policy === "prefer" && verdict.reason === "unsigned")) {
342
+ throw new Error(`Full-v1 publisher verification failed: ${verdict.reason}`);
343
+ }
344
+ }
345
+ const result = await importShard(snapshotDatabaseView(db, manifest.version, files, archive), packTarGz(projectedFiles(files)), { ...options, verifySignature: "trusted-local-only" });
346
+ return { ...result, capability_report: readerCapability(manifest.version, result.capability_report) };
347
+ }
348
+ export async function exportAiwgFortemiKnowledgeShardWithReport(db, options) {
349
+ const version = options.schemaVersion;
350
+ if (options.profile === "full-v1" && version !== "2.2.0") {
351
+ const snapshots = await db.query("SELECT schema_version FROM knowledge_shard_snapshot WHERE profile = 'full-v1'");
352
+ if (snapshots.rows.some(row => row.schema_version === "2.2.0") && !snapshots.rows.some(row => row.schema_version === version)) {
353
+ throw new Error("A 2.2.0/full-v1 snapshot requires schemaVersion: 2.2.0; refusing a lossy downgrade");
354
+ }
355
+ }
356
+ if (version !== "2.1.0" && version !== "2.2.0")
357
+ return exportShardWithReport(db, options);
358
+ if (options.profile !== "full-v1")
359
+ throw new Error(`Unsupported ${version}/${options.profile} reader tuple`);
360
+ const originals = new Map();
361
+ const exists = await db.query("SELECT schema_version FROM knowledge_shard_snapshot WHERE schema_version = $1 AND profile = 'full-v1'", [version]);
362
+ if (exists.rows.length === 0)
363
+ throw new Error(`No persisted ${version}/full-v1 snapshot`);
364
+ const result = await exportShardWithReport(snapshotDatabaseView(db, version, originals), { ...options, profile: "full-v1", schemaVersion: "2.0.0" });
365
+ if (!result.success || !result.archive)
366
+ return { ...result, capability_report: readerCapability(version, result.capability_report) };
367
+ // Core checks current snapshot records and retrieves/verifies mandatory blobs.
368
+ // Rebuild from the original record view so nullable/absent extension fields
369
+ // survive even edits that change only those fields.
370
+ const persisted = await db.query("SELECT component, record_json FROM knowledge_shard_component_record WHERE schema_version = $1 AND profile = 'full-v1' ORDER BY component, ordinal", [version]);
371
+ const records = new Map();
372
+ for (const row of persisted.rows)
373
+ records.set(row.component, [...(records.get(row.component) ?? []), row.record_json]);
374
+ const manifest = parseJson(originals.get("manifest.json"));
375
+ let changed = false;
376
+ for (const component of manifest.components) {
377
+ const filename = originals.has(`${component}.json`) ? `${component}.json` : `${component}.jsonl`;
378
+ const bytes = originals.get(filename);
379
+ const source = filename.endsWith(".json") ? parseJson(bytes)
380
+ : decoder.decode(bytes).split("\n").filter(Boolean).map(line => JSON.parse(line));
381
+ const current = records.get(component) ?? [];
382
+ if (JSON.stringify(source) === JSON.stringify(current))
383
+ continue;
384
+ // JSONB changes key order. Compare structurally before rewriting bytes.
385
+ if (isDeepStrictEqual(source, current))
386
+ continue;
387
+ const updated = filename.endsWith(".json") ? encodeJson(current)
388
+ : encoder.encode(current.map(record => JSON.stringify(record)).join("\n"));
389
+ originals.set(filename, updated);
390
+ manifest.checksums[filename] = digest(updated);
391
+ if (component === "communities") {
392
+ manifest.counts.community_sets = current.length;
393
+ manifest.counts.communities = current.reduce((total, set) => total + set.communities.length, 0);
394
+ }
395
+ else
396
+ manifest.counts[component] = current.length;
397
+ changed = true;
398
+ }
399
+ if (changed) {
400
+ originals.delete("signature.json");
401
+ originals.set("manifest.json", encodeJson(manifest));
402
+ }
403
+ for (const [filename, bytes] of unpackTarGz(result.archive)) {
404
+ if (filename.startsWith("blobs/"))
405
+ originals.set(filename, bytes);
406
+ }
407
+ const archive = packTarGz(originals);
408
+ const validation = await validateAiwgFortemiKnowledgeShard(archive);
409
+ return {
410
+ ...result,
411
+ success: validation.valid,
412
+ archive: validation.valid ? archive : null,
413
+ errors: validation.errors,
414
+ capability_report: readerCapability(version, result.capability_report),
415
+ };
416
+ }
133
417
  //# sourceMappingURL=fortemi-shard-export.js.map
@@ -2,6 +2,7 @@
2
2
  import os from 'node:os';
3
3
  import { fork } from 'node:child_process';
4
4
  import { signalChild } from '../process/signal-child.mjs';
5
+ import { helperEnvironment, DEPLOYMENT_HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
5
6
  const MIB = 1024 ** 2;
6
7
  const WORKER_HEAP_MB = 256;
7
8
  // Leave room for V8's young generation, executable code, IPC and native allocations.
@@ -68,6 +69,7 @@ export async function runIndexPreparation(jobs, serial, moduleUrl, requested, on
68
69
  // Node during allocation before its isolate-limit error is delivered.
69
70
  // A child heap fatal/SIGABRT is instead an ordinary, recoverable exit.
70
71
  const worker = fork(new URL('./index-worker.mjs', import.meta.url), [moduleUrl], {
72
+ env: helperEnvironment(process.env, DEPLOYMENT_HELPER_ENV_ALLOWLIST),
71
73
  execArgv: [`--max-old-space-size=${WORKER_HEAP_MB}`, '--max-semi-space-size=16'],
72
74
  stdio: ['ignore', 'ignore', 'pipe', 'ipc'], serialization: 'advanced',
73
75
  });
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { spawn } from "node:child_process";
4
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST, DESKTOP_HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
4
5
  const random = (bytes = 32) => randomBytes(bytes).toString("base64url");
5
6
  export const createPkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
6
7
  async function responseJson(response) {
@@ -26,7 +27,10 @@ export const defaultBrowserOpener = async (url) => {
26
27
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd.exe" : "xdg-open";
27
28
  const args = process.platform === "win32" ? ["/d", "/s", "/c", "start", "", url] : [url];
28
29
  await new Promise((resolve, reject) => {
29
- const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
30
+ const child = spawn(command, args, {
31
+ detached: true, stdio: "ignore", windowsHide: true,
32
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, ...DESKTOP_HELPER_ENV_ALLOWLIST]),
33
+ });
30
34
  child.once("error", reject);
31
35
  child.once("spawn", () => { child.unref(); resolve(); });
32
36
  });
@@ -1,10 +1,14 @@
1
1
  import { signalChild } from '../process/signal-child.mjs';
2
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST, DESKTOP_HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
2
3
  import { spawn } from "node:child_process";
3
4
  import fs from "node:fs/promises";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  export const defaultCommandRunner = (command, args, stdin = "") => new Promise((resolve, reject) => {
7
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
8
+ const child = spawn(command, args, {
9
+ stdio: ["pipe", "pipe", "pipe"], windowsHide: true,
10
+ env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, ...DESKTOP_HELPER_ENV_ALLOWLIST]),
11
+ });
8
12
  const stdout = [];
9
13
  const stderr = [];
10
14
  let outputBytes = 0;
@@ -0,0 +1,151 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open } from 'node:fs/promises';
3
+ import { isInteractive } from '../cli/env.js';
4
+ import { askMultilineSecret, askString, createSecretPromptInterface } from '../cli/prompt-utils.js';
5
+ import { createSecretStore } from './credential-store.js';
6
+ /** Named raw values share the native store used by auth login, in a separate service. */
7
+ export const SENSITIVE_VALUE_SERVICE = 'aiwg.sensitive-values';
8
+ export function namedSensitiveStore(name) {
9
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(name)) {
10
+ throw new Error('Sensitive value store name must contain only letters, digits, dots, underscores or hyphens (1–128 characters).');
11
+ }
12
+ return createSecretStore({ service: SENSITIVE_VALUE_SERVICE, account: name });
13
+ }
14
+ /** Extract references, never literal values, from the standard command options. */
15
+ export function sensitiveInputOptions(args, prefix = 'value') {
16
+ const result = { prefix };
17
+ for (let index = 0; index < args.length; index += 1) {
18
+ const token = args[index];
19
+ if (token === '--')
20
+ break;
21
+ for (const source of ['file', 'store', 'env']) {
22
+ const flag = `--${prefix}-${source}`;
23
+ if (token !== flag && !token.startsWith(`${flag}=`))
24
+ continue;
25
+ const value = token === flag ? args[++index] : token.slice(flag.length + 1);
26
+ if (!value || value.startsWith('--'))
27
+ throw new Error(`${flag} requires a value`);
28
+ if (result[source] !== undefined)
29
+ throw new Error(`${flag} may only be given once`);
30
+ result[source] = value;
31
+ }
32
+ if (token === `--${prefix}-stdin`)
33
+ result.stdin = true;
34
+ if (token === `--${prefix}-interactive`)
35
+ result.interactive = true;
36
+ }
37
+ return result;
38
+ }
39
+ /** Extracted from MCP profile login: validate the opened descriptor, never a later path read. */
40
+ export async function readPrivateValueFile(filePath, flag = '--value-file') {
41
+ if (process.platform === 'win32')
42
+ throw new Error(`${flag}: private-file input requires native ACL validation on Windows; use stdin or the OS store.`);
43
+ try {
44
+ const pre = await lstat(filePath);
45
+ if (pre.isSymbolicLink())
46
+ throw new Error('file is a symlink; refusing to follow it');
47
+ // Reject FIFOs/devices before open, which could otherwise wait indefinitely.
48
+ if (!pre.isFile())
49
+ throw new Error('not a regular file');
50
+ }
51
+ catch (error) {
52
+ const code = error.code;
53
+ throw new Error(`${flag}: ${code === 'ENOENT' ? 'file does not exist' : code ? 'cannot stat private file' : error.message}.`);
54
+ }
55
+ let handle;
56
+ try {
57
+ handle = await open(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
58
+ }
59
+ catch {
60
+ throw new Error(`${flag}: cannot open private file (symlinks are refused).`);
61
+ }
62
+ try {
63
+ const stat = await handle.stat();
64
+ if (!stat.isFile())
65
+ throw new Error(`${flag}: not a regular file.`);
66
+ if (stat.nlink !== 1)
67
+ throw new Error(`${flag}: more than one hard link; refusing to read it.`);
68
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
69
+ throw new Error(`${flag}: file is not owned by the current user.`);
70
+ }
71
+ if ((stat.mode & 0o077) !== 0)
72
+ throw new Error(`${flag}: file must not allow group or other access (chmod 600).`);
73
+ return (await handle.readFile({ encoding: 'utf8' })).trim();
74
+ }
75
+ finally {
76
+ await handle.close();
77
+ }
78
+ }
79
+ async function readStdin() {
80
+ const chunks = [];
81
+ for await (const chunk of process.stdin)
82
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
83
+ return Buffer.concat(chunks).toString('utf8');
84
+ }
85
+ /**
86
+ * Explicit entry, stdin, private file, native-store reference, named shell variable.
87
+ * No implicit stdin consumption, env discovery or store probing. An empty selected
88
+ * source is an error, never permission to try a less preferred source.
89
+ */
90
+ export async function resolveSensitiveInput(options = {}, dependencies = {}) {
91
+ const prefix = options.prefix ?? 'value';
92
+ const terminal = dependencies.isTTY ?? isInteractive();
93
+ const choices = `--${prefix}-interactive, --${prefix}-stdin, --${prefix}-file <path>, --${prefix}-store <name>, or --${prefix}-env <name>`;
94
+ const explicitSource = options.stdin || options.file !== undefined || options.store !== undefined || options.env !== undefined;
95
+ let value;
96
+ let source;
97
+ if (options.interactive || (!explicitSource && terminal)) {
98
+ if (!terminal)
99
+ throw new Error(`An interactive terminal is required; supply ${choices}.`);
100
+ source = `--${prefix}-interactive`;
101
+ if (dependencies.prompt)
102
+ value = await dependencies.prompt(prefix);
103
+ else {
104
+ const rl = createSecretPromptInterface();
105
+ try {
106
+ process.stderr.write(`Enter ${prefix} (hidden${options.multiline ? '; finish with an empty line' : ''}): `);
107
+ value = options.multiline ? await askMultilineSecret(rl, options.signal) : await askString(rl, '', '', options.signal);
108
+ }
109
+ finally {
110
+ rl.close();
111
+ process.stderr.write('\n');
112
+ }
113
+ }
114
+ }
115
+ else if (options.stdin) {
116
+ source = `--${prefix}-stdin`;
117
+ if (dependencies.stdinIsTTY ?? dependencies.isTTY ?? Boolean(process.stdin.isTTY))
118
+ throw new Error(`${source} requires piped input, not a terminal.`);
119
+ value = await (dependencies.readStdin ?? readStdin)();
120
+ }
121
+ else if (options.file !== undefined) {
122
+ source = `--${prefix}-file`;
123
+ value = await (dependencies.readFile ? dependencies.readFile(options.file) : readPrivateValueFile(options.file, source));
124
+ }
125
+ else if (options.store !== undefined) {
126
+ source = `--${prefix}-store`;
127
+ try {
128
+ value = await (dependencies.readStore ? dependencies.readStore(options.store) : namedSensitiveStore(options.store).loadSecret());
129
+ }
130
+ catch {
131
+ throw new Error(`${source}: native store entry could not be read.`);
132
+ }
133
+ }
134
+ else if (options.env !== undefined) {
135
+ source = `--${prefix}-env`;
136
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(options.env))
137
+ throw new Error(`${source} requires a shell variable name.`);
138
+ value = dependencies.readEnv ? dependencies.readEnv(options.env) : (dependencies.environment ?? process.env)[options.env];
139
+ }
140
+ else {
141
+ throw new Error(`A sensitive value requires an interactive terminal, or a key supplied via ${choices}.`);
142
+ }
143
+ const trimmed = value?.trim();
144
+ if (!trimmed)
145
+ throw new Error(`${source} received no key (input is empty or not set${options.env && source.endsWith('-env') ? `: ${options.env}` : ''}).`);
146
+ const controls = options.multiline ? /[\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f]/ : /[\u0000-\u001f\u007f]/;
147
+ if (trimmed.length > 16384 || controls.test(trimmed))
148
+ throw new Error(`${source} requires ${options.multiline ? 'non-empty text without control characters' : 'one non-empty line'} (at most 16384 characters).`);
149
+ return trimmed;
150
+ }
151
+ //# sourceMappingURL=sensitive-input.js.map
@@ -18,6 +18,7 @@ import { spawn } from 'child_process';
18
18
  import os from 'os';
19
19
  import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
20
20
  import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
21
+ import { helperEnvironment } from '../process/cli-command.mjs';
21
22
  import {
22
23
  assertCanonicalInstallation,
23
24
  createInstallationIdentity,
@@ -594,10 +595,12 @@ export async function getVersionInfo() {
594
595
  try {
595
596
  const gitHash = boundedExecFileSync('git', ['rev-parse', '--short', 'HEAD'], 'quick', {
596
597
  cwd: config.edgePath,
598
+ env: helperEnvironment(),
597
599
  encoding: 'utf8',
598
600
  }).trim();
599
601
  const gitBranch = boundedExecFileSync('git', ['branch', '--show-current'], 'quick', {
600
602
  cwd: config.edgePath,
603
+ env: helperEnvironment(),
601
604
  encoding: 'utf8',
602
605
  }).trim();
603
606
  info.edgePath = config.edgePath;