@aiwg/cli 2026.10.6 → 2026.10.8

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 (100) 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/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -4,7 +4,9 @@ import { JevDecisionAdapter, compileJevQuestion } from '../adapters/jev.js';
4
4
  import { DecisionAdmissionRegistry } from '../admission.js';
5
5
  import { CanonicalJsonByteEstimator } from '../context-plan.js';
6
6
  import { assertContextLiveSource } from '../context-live-qualification.js';
7
+ import { tokenCost } from '../pricing.js';
7
8
  import { admissionControllerLoadDriver, decisionLoadArrivals, compareDecisionLoadBounds, decisionLoadManifestDigest, decisionLoadPolicy, runDecisionLoadHarness, } from './load.js';
9
+ export const DECISION_STAGED_LOAD_RESULT_VERSIONS = ['decision-load-result/v1', 'decision-load-result/v2'];
8
10
  const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
9
11
  const positive = (value) => finite(value) && value > 0;
10
12
  const exactCommit = (value) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
@@ -289,7 +291,9 @@ export async function runDecisionStagedLoad(options) {
289
291
  onObservation: (arrival, attempt, admission, observation) => {
290
292
  const receipt = inFlight.get(`${arrival.ordinal}:${attempt}`);
291
293
  Object.assign(receipt, { settledAtMs: clock.now(), admission: structuredClone(admission), status: observation.status, reason: observation.reason,
292
- usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus }) });
294
+ usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus }),
295
+ ...(observation.actualModel ? { servedModel: observation.actualModel } : {}),
296
+ ...(observation.requestId ? { requestId: observation.requestId } : {}) });
293
297
  inFlight.delete(`${arrival.ordinal}:${attempt}`);
294
298
  guard.observe(arrival.providerId, observation, receipt.tokens, receipt.costUsd);
295
299
  },
@@ -335,7 +339,7 @@ export async function runDecisionStagedLoad(options) {
335
339
  host: { hostname: hostname(), cpuClass: `${cpus().length} cores; ${cpus()[0]?.model ?? 'unknown'}`, memoryGiB: totalmem() / 2 ** 30,
336
340
  networkPath: options.networkPath, node: process.version, platform: process.platform, arch: process.arch },
337
341
  pricing: options.pricing, stages, receipts, samples, profileHistory: registry.profileHistory('load-harness'), breakerTransitions: transitions,
338
- usage: guard.usage, stop: guard.stop, startedAtMs: start, elapsedMs: samples.at(-1)?.atMs ?? clock.now() - start });
342
+ reservedUsage: guard.usage, stop: guard.stop, startedAtMs: start, elapsedMs: samples.at(-1)?.atMs ?? clock.now() - start });
339
343
  }
340
344
  /** Token-bucket debit (burst plus refill), matching the admission controller's rate semantics. */
341
345
  function bucketDebit(receipts, capacity, periodMs, amount) {
@@ -400,9 +404,9 @@ export function compareDecisionStagedLoad(record) {
400
404
  }
401
405
  }
402
406
  }
403
- atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.usage.usd);
404
- atMost('budget.maxCalls', m.budget.maxCalls, record.usage.calls);
405
- atMost('budget.maxTokens', m.budget.maxTokens, record.usage.tokens);
407
+ atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.reservedUsage.usd);
408
+ atMost('budget.maxCalls', m.budget.maxCalls, record.reservedUsage.calls);
409
+ atMost('budget.maxTokens', m.budget.maxTokens, record.reservedUsage.tokens);
406
410
  atMost('budget.maxWallClockSeconds', m.budget.maxWallClockSeconds, record.elapsedMs / 1000);
407
411
  atMost('maximumSampleGapMs', m.bounds.maximumSampleGapMs, maximum(record.samples.slice(1).map((s, i) => s.atMs - record.samples[i].atMs)));
408
412
  const endingAt = record.startedAtMs + record.elapsedMs;
@@ -440,10 +444,29 @@ export function compareDecisionStagedLoad(record) {
440
444
  atMost('stop.maximumCrossPrincipalQuotaViolations', 0, violations);
441
445
  return out;
442
446
  }
447
+ function reportedUsage(receipts, pricing) {
448
+ const sum = (values) => values.reduce((total, value) => total === null || value === null ? null : total + value, 0);
449
+ const inputs = receipts.map(r => r.usage.inputTokens);
450
+ const outputs = receipts.map(r => r.usage.outputTokens);
451
+ const totals = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null ? null : r.usage.inputTokens + r.usage.outputTokens);
452
+ const inputTokens = sum(inputs);
453
+ const outputTokens = sum(outputs);
454
+ // The request is pinned to the priced model; an explicitly different served model has no applicable rate.
455
+ const costs = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null
456
+ || (r.servedModel !== undefined && r.servedModel !== pricing.jev_model) ? null
457
+ : tokenCost(r.usage, { inputPerMillionUsd: pricing.jev_input_per_mtok, outputPerMillionUsd: pricing.jev_output_per_mtok }));
458
+ const knownSum = (values) => values.length > 0 && values.every(v => v === null)
459
+ ? null : sum(values.filter(v => v !== null));
460
+ return { calls: receipts.length, inputTokens, outputTokens, tokens: inputTokens === null || outputTokens === null ? null : inputTokens + outputTokens,
461
+ usd: sum(costs), incompleteCalls: costs.filter(cost => cost === null).length,
462
+ knownSubtotal: { inputTokens: knownSum(inputs), outputTokens: knownSum(outputs), tokens: knownSum(totals), usd: knownSum(costs) } };
463
+ }
443
464
  export function decisionStagedLoadResultRecord(input) {
444
465
  const manifest = readDecisionLoadManifest(input.manifestBytes);
445
466
  validateDecisionLoadManifestV3(manifest);
446
- const payload = { ...input, schemaVersion: 'decision-load-result/v1', kind: 'load-manifest-qualified',
467
+ validateDecisionLoadPricing(input.pricing, manifest.model);
468
+ const payload = { ...input, schemaVersion: 'decision-load-result/v2', kind: 'load-manifest-qualified',
469
+ reportedUsage: reportedUsage(input.receipts, input.pricing),
447
470
  manifest, manifestDigest: decisionLoadManifestDigest(input.manifestBytes) };
448
471
  for (const key of ['digest', 'comparisons', 'passed', 'queueWaitDistribution', 'retries'])
449
472
  delete payload[key];
@@ -461,7 +484,7 @@ export function decisionStagedLoadResultRecord(input) {
461
484
  export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
462
485
  try {
463
486
  const r = value;
464
- if (!r || r.schemaVersion !== 'decision-load-result/v1' || r.kind !== 'load-manifest-qualified' || r.mode !== 'staged-provider'
487
+ if (!r || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(r.schemaVersion) || r.kind !== 'load-manifest-qualified' || r.mode !== 'staged-provider'
465
488
  || r.execution !== 'live' || r.dirty !== false || !exactCommit(r.sourceCommit) || (sourceCommit !== undefined && sourceCommit !== r.sourceCommit)
466
489
  || r.stop !== null || !r.passed || !finite(r.startedAtMs) || !r.host.networkPath || !r.host.cpuClass || !positive(r.host.memoryGiB))
467
490
  return false;
@@ -512,8 +535,17 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
512
535
  || !positive(receipt.tokens) || !finite(receipt.costUsd) || !finite(receipt.admittedAtMs) || receipt.settledAtMs < receipt.admittedAtMs
513
536
  || receipt.admission.decision !== 'admit' || receipt.costUsd !== price(receipt.tokens, r.pricing)))
514
537
  return false;
515
- if (r.usage.calls !== r.receipts.length || r.usage.tokens !== r.receipts.reduce((sum, receipt) => sum + receipt.tokens, 0)
516
- || Math.abs(r.usage.usd - r.receipts.reduce((sum, receipt) => sum + receipt.costUsd, 0)) > 1e-9)
538
+ const reservedUsage = r.schemaVersion === 'decision-load-result/v1' ? r.usage : r.reservedUsage;
539
+ if (!reservedUsage || ![reservedUsage.calls, reservedUsage.tokens, reservedUsage.usd].every(finite)
540
+ || reservedUsage.calls !== r.receipts.length || reservedUsage.tokens !== r.receipts.reduce((sum, receipt) => sum + receipt.tokens, 0)
541
+ || Math.abs(reservedUsage.usd - r.receipts.reduce((sum, receipt) => sum + receipt.costUsd, 0)) > 1e-9)
542
+ return false;
543
+ if (r.schemaVersion === 'decision-load-result/v2' && r.receipts.some(receipt => !receipt.usage
544
+ || ![receipt.usage.inputTokens, receipt.usage.outputTokens].every(v => v === null || (Number.isSafeInteger(v) && v >= 0))
545
+ || (receipt.usage.inputTokens ?? 0) + (receipt.usage.outputTokens ?? 0) > receipt.tokens
546
+ || (receipt.usage.costUsd !== null && (!finite(receipt.usage.costUsd) || receipt.usage.costUsd > receipt.costUsd + 1e-12))
547
+ || (receipt.servedModel !== undefined && (typeof receipt.servedModel !== 'string' || !receipt.servedModel))
548
+ || (receipt.requestId !== undefined && (typeof receipt.requestId !== 'string' || !receipt.requestId))))
517
549
  return false;
518
550
  if (!r.samples.every((sample, index) => finite(sample.atMs) && finite(sample.residentMemoryMiB) && finite(sample.heapUsedMiB)
519
551
  && finite(sample.cpuPercentOneCore) && finite(sample.active) && finite(sample.queued) && (index === 0 ? sample.atMs === 0 : sample.atMs >= r.samples[index - 1].atMs
@@ -529,7 +561,12 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
529
561
  || change?.kind !== 'change' || change.profileVersion !== manifest.profileVersion || change.digest !== profileDigest(manifest.workspace, manifest.providers)
530
562
  || rollback?.kind !== 'rollback' || rollback.profileVersion !== initial.profileVersion || rollback.digest !== initial.digest)
531
563
  return false;
532
- const rebuilt = decisionStagedLoadResultRecord(r);
564
+ const rebuilt = decisionStagedLoadResultRecord({ ...r, reservedUsage });
565
+ if (r.schemaVersion === 'decision-load-result/v1') {
566
+ const { reservedUsage: _, reportedUsage: __, digest: ___, ...legacy } = rebuilt;
567
+ const legacyBody = { ...legacy, schemaVersion: r.schemaVersion };
568
+ return rebuilt.passed && canonicalJson({ ...legacyBody, digest: decisionLoadManifestDigest(canonicalJson(legacyBody)) }) === canonicalJson(r);
569
+ }
533
570
  return rebuilt.passed && canonicalJson(rebuilt) === canonicalJson(r);
534
571
  }
535
572
  catch {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { lstat, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import { dirname, resolve, sep } from 'node:path';
4
+ import { DECISION_STAGED_LOAD_RESULT_VERSIONS } from './load.js';
4
5
  import { evaluateQualification } from './gates.js';
5
6
  import { deriveGateEvidence, DERIVED_GATE_EVIDENCE, GATE_ARTIFACT_FLAGS, GATE_ARTIFACT_SCHEMAS, IMMUTABLE_SPLIT_VERSIONS, validateGateArtifact, } from './gate-evidence.js';
6
7
  import { scanQualificationPrivacy } from './privacy.js';
@@ -233,7 +234,7 @@ async function attachGateArtifacts(plan, evidence, maxBytes) {
233
234
  const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
234
235
  await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
235
236
  await rename(temporary, target);
236
- refs[flag] = { artifact: relative, digest: digest(bytes), schemaVersion: GATE_ARTIFACT_SCHEMAS[flag] };
237
+ refs[flag] = { artifact: relative, digest: digest(bytes), schemaVersion: parsed.schemaVersion };
237
238
  context.accepted[flag] = parsed;
238
239
  }
239
240
  return refs;
@@ -269,8 +270,11 @@ export async function verifyQualificationGateArtifacts(manifest, artifactRoot, m
269
270
  const parsed = parseJson(bytes);
270
271
  const versionOk = flag === 'immutable-splits'
271
272
  ? IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
272
- : ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
273
- if (!versionOk || !validateGateArtifact(flag, parsed, context)) {
273
+ : flag === 'load-manifest-qualified'
274
+ ? DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(ref.schemaVersion)
275
+ : ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
276
+ if (!versionOk || (flag === 'load-manifest-qualified' && parsed?.schemaVersion !== ref.schemaVersion)
277
+ || !validateGateArtifact(flag, parsed, context)) {
274
278
  results.push({ flag, verified: false, reason: 'invalid-artifact' });
275
279
  continue;
276
280
  }
@@ -48,11 +48,14 @@ export function credentialReference(input, paths = { isAbsolute }) {
48
48
  else if (input.startsWith('env:') && /^[A-Za-z_][A-Za-z0-9_]*$/.test(input.slice(4))) {
49
49
  reference = { kind: 'credential-ref', source: 'env', name: input.slice(4) };
50
50
  }
51
+ else if (input.startsWith('store:') && /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(input.slice(6))) {
52
+ reference = { kind: 'credential-ref', source: 'store', name: input.slice(6) };
53
+ }
51
54
  else if (input.startsWith('file:') && paths.isAbsolute(input.slice(5))) {
52
55
  reference = { kind: 'credential-ref', source: 'file', name: input.slice(5) };
53
56
  }
54
57
  else
55
- throw new Error('--credential requires <provider>=env:NAME|file:PATH|stdin (file paths must be absolute).');
58
+ throw new Error('--credential requires <provider>=stdin|file:PATH|store:NAME|env:NAME (file paths must be absolute).');
56
59
  assertPlanRedacted(reference);
57
60
  return reference;
58
61
  }
@@ -106,9 +109,10 @@ export function interviewQuestions(context, answers = {}) {
106
109
  const configPath = (targetRoot === project.root ? project.providerFiles.find(f => f.provider === provider && f.kind === 'config')?.path : undefined)
107
110
  ?? (resolveProviderPathValue(getProviderDefinition(provider)?.paths.configFile ?? null, targetRoot, paths) || targetRoot);
108
111
  add('locations', 'config-dir', "Where this provider's config file lives (informational; A3 writes nothing).", configPath, [configPath], '--target <dir>', 'path', project.root, provider);
109
- add('locations', 'credential-source', "Where should A4 read this provider's credential from?", null, [{ kind: 'credential-ref', source: 'env', name: credentialEnvName(provider) },
112
+ add('locations', 'credential-source', "Where should A4 read this provider's credential from?", null, [{ kind: 'credential-ref', source: 'stdin-at-apply', name: 'stdin' },
110
113
  { kind: 'credential-ref', source: 'file', name: paths.join(project.root, '.credentials', provider) },
111
- { kind: 'credential-ref', source: 'stdin-at-apply', name: 'stdin' }], '--credential <provider>=env:NAME|file:PATH|stdin', 'credential', project.root, provider, true);
114
+ { kind: 'credential-ref', source: 'store', name: provider },
115
+ { kind: 'credential-ref', source: 'env', name: credentialEnvName(provider) }], '--credential <provider>=stdin|file:PATH|store:NAME|env:NAME', 'credential', project.root, provider, true);
112
116
  }
113
117
  for (const project of projects) {
114
118
  const rootChoice = answers[questionId('targets', 'root', project.root)]?.value;
@@ -127,8 +131,9 @@ export function interviewQuestions(context, answers = {}) {
127
131
  const jevStatus = context.jevStatus?.[targetWorkspace];
128
132
  add('capabilities', 'jev', `Jev status: ${jevStatus ? jevStatus.enabled ? 'enabled' : 'off' : 'unknown'}; backend ${jevStatus?.backend.status ?? 'not-probed'}. Decisions are sent to an external service; a Jev account/API access and usage charges apply. Enable decision offload?`, false, ['enable', 'decline'], '--jev enable|decline|defer', 'choice', project.root, undefined, true);
129
133
  if (jev === 'enable') {
130
- add('capabilities', 'credential-source', 'Where should A4 read the Jev API input from? Enter a reference only; setup/defer works without an API input.', null, [{ kind: 'credential-ref', source: 'env', name: 'JEV_API_KEY' }, { kind: 'credential-ref', source: 'file', name: paths.join(project.root, '.credentials', 'jev') },
131
- { kind: 'credential-ref', source: 'stdin-at-apply', name: 'stdin' }], '--credential jev=env:NAME|file:PATH|stdin', 'credential', project.root, 'jev', true);
134
+ add('capabilities', 'credential-source', 'Where should A4 read the Jev API input from? Enter a reference only; setup/defer works without an API input.', null, [{ kind: 'credential-ref', source: 'stdin-at-apply', name: 'stdin' },
135
+ { kind: 'credential-ref', source: 'file', name: paths.join(project.root, '.credentials', 'jev') },
136
+ { kind: 'credential-ref', source: 'store', name: 'jev' }, { kind: 'credential-ref', source: 'env', name: 'JEV_API_KEY' }], '--credential jev=stdin|file:PATH|store:NAME|env:NAME', 'credential', project.root, 'jev', true);
132
137
  questions[questions.length - 1].questionId = questionId('locations', 'credential-source', project.root, 'jev');
133
138
  add('capabilities', 'jev-options', 'Optional Jev settings as JSON: region, endpoint, verify. Verification sends a billable request at apply.', null, [null], '--region <r> --endpoint <url> --verify', 'policy', project.root, 'jev', true);
134
139
  }
@@ -11,9 +11,10 @@
11
11
  *
12
12
  * Credential values are resolved only at apply time and are never stored on
13
13
  * the plan, in receipts, or in rendered output. Only the reference
14
- * (`env:NAME`, `file:PATH`, `stdin`) is ever rendered or persisted.
14
+ * (`env:NAME`, `file:PATH`, `store:NAME`, `stdin`) is ever rendered or persisted.
15
15
  */
16
16
  import { relative, resolve, sep, isAbsolute, join } from 'node:path';
17
+ import { resolveSensitiveInput } from '../auth/sensitive-input.js';
17
18
  import { revalidateSetupPlan, remainingPlanActions } from './plan-store.js';
18
19
  import { questionId } from './interview.js';
19
20
  /** Apply order for plan actions. Dependencies flow config -> verify -> deploy -> register. */
@@ -393,37 +394,32 @@ function formatFactValue(value) {
393
394
  }
394
395
  /** Resolve a credential reference to its value. The value must stay in memory only. */
395
396
  export async function resolveCredential(reference, provider, io) {
396
- // Every message below is receipt-persisted, so wording stays hyphenated:
397
- // the plan redaction gate rejects `credential <value>` phrasing.
398
- if (reference.source === 'env') {
399
- const value = io.readEnv(reference.name);
400
- if (!value) {
401
- return { ok: false, message: `Credential-source env:${reference.name} for ${provider} is not set; export it or re-answer locations.credential-source.` };
402
- }
397
+ try {
398
+ const terminal = reference.source === 'stdin-at-apply' && io.promptCredential !== undefined;
399
+ const value = await resolveSensitiveInput({
400
+ prefix: 'credential',
401
+ ...(reference.source === 'env' ? { env: reference.name } : {}),
402
+ ...(reference.source === 'file' ? { file: reference.name } : {}),
403
+ ...(reference.source === 'store' ? { store: reference.name } : {}),
404
+ ...(reference.source === 'stdin-at-apply' ? terminal ? { interactive: true } : { stdin: true } : {}),
405
+ }, {
406
+ readEnv: io.readEnv,
407
+ readFile: io.readCredentialFile,
408
+ ...(io.readCredentialStore ? { readStore: async (name) => (await io.readCredentialStore(name)) ?? null } : {}),
409
+ // An absent reader explicitly refuses headless stdin; never access the
410
+ // process stream behind an injected apply boundary.
411
+ readStdin: io.readStdinCredential ?? (async () => ''),
412
+ isTTY: terminal,
413
+ ...(io.promptCredential ? { prompt: async () => (await io.promptCredential(`Enter ${provider} API input (kept out of the setup plan): `)) ?? '' } : {}),
414
+ });
403
415
  return { ok: true, value };
404
416
  }
405
- if (reference.source === 'file') {
406
- try {
407
- const value = (await io.readCredentialFile(reference.name)).trim();
408
- if (!value)
409
- return { ok: false, message: `Credential-file ${reference.name} for ${provider} is empty; fix the file or re-answer locations.credential-source.` };
410
- return { ok: true, value };
411
- }
412
- catch {
413
- return { ok: false, message: `Credential-file ${reference.name} for ${provider} is unreadable; fix the path or re-answer locations.credential-source.` };
414
- }
415
- }
416
- if (io.readStdinCredential) {
417
- const value = (await io.readStdinCredential()).trim();
418
- return value ? { ok: true, value } : { ok: false, message: `No stdin-at-apply input for ${provider}; retry with env:/file:.` };
419
- }
420
- if (!io.promptCredential) {
421
- return { ok: false, message: `Stdin-at-apply input for ${provider} needs an interactive terminal; re-run interactively or re-answer with env:/file:.` };
417
+ catch {
418
+ // Source failures can contain arbitrary external text. Receipts persist
419
+ // only the locator and supported alternatives, never the raw failure.
420
+ const source = reference.source === 'stdin-at-apply' ? 'Stdin-at-apply' : `Credential-source ${reference.source}:${reference.name}`;
421
+ return { ok: false, message: `${source} for ${provider} is unavailable; use --credential ${provider}=stdin|file:PATH|store:NAME|env:NAME (stdin prompts at a terminal).` };
422
422
  }
423
- const value = await io.promptCredential(`Enter ${provider} API input (kept out of the setup plan): `);
424
- if (!value)
425
- return { ok: false, message: `No stdin-at-apply input given for ${provider}; the action stays eligible for a later run.` };
426
- return { ok: true, value };
427
423
  }
428
424
  function receiptFor(plan, action, status, message, now) {
429
425
  const next = { ...plan, updatedAt: now, receipts: plan.receipts.map(receipt => ({ ...receipt, actions: { ...receipt.actions } })) };
@@ -449,7 +445,7 @@ async function executePlanAction(plan, action, io, advisoryNote) {
449
445
  if (!remove) {
450
446
  reference = credentialReferenceFor(plan, action.workspace, 'jev');
451
447
  if (!reference) {
452
- return { status: 'skipped', message: 'Jev setup deferred: choose --credential jev=env:NAME|file:PATH|stdin.' };
448
+ return { status: 'skipped', message: 'Jev setup deferred: choose --credential jev=stdin|file:PATH|store:NAME|env:NAME.' };
453
449
  }
454
450
  const resolved = await resolveCredential(reference, 'jev', io);
455
451
  if (!resolved.ok) {
@@ -7,7 +7,7 @@ const timestamp = dateTime({ offset: true });
7
7
  const observation = z.enum(['yes', 'no', 'unknown']);
8
8
  export const planIdSchema = text.max(120).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
9
9
  const referenceSchema = z.strictObject({
10
- kind: z.literal('credential-ref'), source: z.enum(['env', 'file', 'stdin-at-apply']), name: text,
10
+ kind: z.literal('credential-ref'), source: z.enum(['env', 'file', 'store', 'stdin-at-apply']), name: text,
11
11
  });
12
12
  const valueSchema = z.lazy(() => z.union([
13
13
  z.string(), z.number().finite(), z.boolean(), z.null(), z.array(valueSchema), z.record(z.string(), valueSchema),
@@ -57,7 +57,7 @@ const planSchema = z.strictObject({
57
57
  inventory: inventorySchema,
58
58
  });
59
59
  const sensitiveKey = /(?:password|passwd|token|secret|api[_-]?key|credential|authorization|cookie|private[_-]?key|connection[_-]?string)/i;
60
- const secretError = () => new Error('Setup plan contains secret-looking material; use a credential-ref (env, file, or stdin-at-apply).');
60
+ const secretError = () => new Error('Setup plan contains secret-looking material; use a credential-ref (env, file, store, or stdin-at-apply).');
61
61
  /** Scan the exact JSON snapshot, including keys and free-form evidence/messages. Never echo rejected values. */
62
62
  export function assertPlanRedacted(input) {
63
63
  let nodes = 0;
@@ -0,0 +1,91 @@
1
+ /** Shared host configuration for the effect ledger and its signing consumers. */
2
+ import { basename, resolve } from 'node:path';
3
+ import { createSecretStore } from '../auth/credential-store.js';
4
+ import { readAiwgConfig } from '../config/aiwg-config.js';
5
+ import { DEFAULT_LEDGER_KEY_SERVICE, LEDGER_TEST_KEY_ENV, credentialStoreKeyProvider, environmentTestKeyProvider } from './keys.js';
6
+ import { EffectLedgerError, usageError as usage } from './errors.js';
7
+ import { openEffectLedger } from './ledger.js';
8
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$/;
9
+ function defaultProject(projectDir) {
10
+ const cleaned = basename(resolve(projectDir)).replace(/[^A-Za-z0-9._:/@-]/g, '-').replace(/^[^A-Za-z0-9]+/, '');
11
+ return cleaned && IDENTIFIER.test(cleaned) ? cleaned : 'project';
12
+ }
13
+ async function hostConfig(projectDir) {
14
+ let config;
15
+ try {
16
+ config = await readAiwgConfig(projectDir);
17
+ }
18
+ catch {
19
+ throw new EffectLedgerError('usage', 'aiwg.config could not be read', 'config-unreadable');
20
+ }
21
+ const effects = config?.effects;
22
+ if (effects === undefined || effects === null)
23
+ return {};
24
+ if (typeof effects !== 'object' || Array.isArray(effects))
25
+ throw usage('aiwg.config effects must be an object', 'config-invalid');
26
+ return effects;
27
+ }
28
+ function testContext(env) {
29
+ return env.VITEST === 'true' || env.NODE_ENV === 'test' || env.CI === 'true' || env.CI === '1';
30
+ }
31
+ export function keyStore(session, role) {
32
+ const { deps, config, account } = session;
33
+ if (deps.keyStore)
34
+ return deps.keyStore(role, account);
35
+ if (session.env[LEDGER_TEST_KEY_ENV] && testContext(session.env))
36
+ return null;
37
+ const provider = config.keyProvider ?? {};
38
+ try {
39
+ if (provider.type === 'file') {
40
+ if (!provider.path)
41
+ throw new Error('missing path');
42
+ const pathname = resolve(session.projectDir, provider.path);
43
+ return createSecretStore({ useFile: true, allowFile: true, pathname: role === 'next' ? `${pathname}.next` : pathname, service: DEFAULT_LEDGER_KEY_SERVICE, account });
44
+ }
45
+ if (provider.type !== undefined && provider.type !== 'credential-store')
46
+ throw new Error('unknown provider');
47
+ return createSecretStore({ service: provider.service ?? DEFAULT_LEDGER_KEY_SERVICE, account: role === 'next' ? `${account}.next` : account });
48
+ }
49
+ catch {
50
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key provider is not configured correctly on this host', 'key-provider-invalid');
51
+ }
52
+ }
53
+ export function keyProvider(session) {
54
+ const store = keyStore(session, 'current');
55
+ if (!store)
56
+ return environmentTestKeyProvider(session.env);
57
+ return credentialStoreKeyProvider({ account: session.account, store });
58
+ }
59
+ export function describeKeyProvider(session) {
60
+ if (session.deps.keyStore)
61
+ return { type: 'injected', account: session.account };
62
+ if (session.env[LEDGER_TEST_KEY_ENV] && testContext(session.env))
63
+ return { type: 'environment-test-key' };
64
+ const provider = session.config.keyProvider ?? {};
65
+ if (provider.type === 'file')
66
+ return { type: 'file', account: session.account };
67
+ return { type: 'credential-store', service: provider.service ?? DEFAULT_LEDGER_KEY_SERVICE, account: session.account };
68
+ }
69
+ export function openLedger(session, registry) {
70
+ const writer = session.config.writer ?? 'cli';
71
+ return openEffectLedger({
72
+ projectDir: session.projectDir, scope: session.scope, writer, keyProvider: keyProvider(session),
73
+ ...(session.deps.clock ? { clock: session.deps.clock } : {}),
74
+ ...(session.deps.sink ? { sink: session.deps.sink } : {}),
75
+ ...(registry ? { verifiers: registry } : {}),
76
+ ...(session.deps.lockTimeoutMs ? { lockTimeoutMs: session.deps.lockTimeoutMs } : {}),
77
+ });
78
+ }
79
+ /** Resolve the same host scope and key provider for CLI and qualification evidence. */
80
+ export async function effectSession(projectDir, subsystem = 'delivery', deps = {}) {
81
+ const config = await hostConfig(projectDir);
82
+ const tenant = config.tenant ?? 'local';
83
+ const project = config.project ?? defaultProject(projectDir);
84
+ if (!IDENTIFIER.test(tenant) || !IDENTIFIER.test(project))
85
+ throw usage('aiwg.config effects tenant and project must be identifiers', 'config-invalid');
86
+ const scope = { tenant, project, subsystem };
87
+ const env = deps.env ?? process.env;
88
+ const account = config.keyProvider?.account ?? `ledger/${tenant}/${project}/${subsystem}`;
89
+ return { projectDir, scope, config, account, deps, env };
90
+ }
91
+ //# sourceMappingURL=host.js.map
@@ -687,7 +687,7 @@ export const initCommand = {
687
687
  triggerPhrases: ['init project', 'setup project config', 'configure providers'],
688
688
  commandHint: {
689
689
  template: 'utility',
690
- argumentHint: '[--scope project|user] [--intent code|docs|ops|mixed] [--credential <provider>=env:NAME|file:PATH|stdin] [--jev enable|decline|defer]',
690
+ argumentHint: '[--scope project|user] [--intent code|docs|ops|mixed] [--credential <provider>=stdin|file:PATH|store:NAME|env:NAME] [--jev enable|decline|defer]',
691
691
  allowedTools: ['Read', 'Write'],
692
692
  },
693
693
  },
@@ -719,7 +719,7 @@ export const setupCommand = {
719
719
  ],
720
720
  commandHint: {
721
721
  template: 'utility',
722
- argumentHint: 'project [--yes] [--dry-run] [--review] [--apply] [--plan <id>] [--target <dir>] [--scope project|user] [--workspace <root>] [--monorepo root|package] [--package <root>] [--scan-roots <dir>] [--intent code|docs|ops|mixed] [--credential <provider>=env:NAME|file:PATH|stdin] [--jev enable|decline|defer]',
722
+ argumentHint: 'project [--yes] [--dry-run] [--review] [--apply] [--plan <id>] [--target <dir>] [--scope project|user] [--workspace <root>] [--monorepo root|package] [--package <root>] [--scan-roots <dir>] [--intent code|docs|ops|mixed] [--credential <provider>=stdin|file:PATH|store:NAME|env:NAME] [--jev enable|decline|defer]',
723
723
  allowedTools: ['Read', 'Write', 'Bash'],
724
724
  },
725
725
  },
@@ -21,6 +21,7 @@
21
21
  import { readFile, writeFile, access } from 'fs/promises';
22
22
  import { join } from 'path';
23
23
  import { execFile } from 'child_process';
24
+ import { helperEnvironment } from '../process/cli-command.mjs';
24
25
  import { promisify } from 'util';
25
26
  const execFileP = promisify(execFile);
26
27
  /**
@@ -166,6 +167,7 @@ export async function checkBundleManifestIgnored(projectDir, manifestRelPath) {
166
167
  // 1 if not ignored, 128 on error. We want a tristate.
167
168
  await execFileP('git', ['check-ignore', '-q', manifestRelPath], {
168
169
  cwd: projectDir,
170
+ env: helperEnvironment(),
169
171
  });
170
172
  // exit 0 → ignored
171
173
  return true;
@@ -12,7 +12,8 @@ import path from 'node:path';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { getFrameworkRoot } from '../channel/manager.mjs';
14
14
  import { readAiwgConfig } from '../config/aiwg-config.js';
15
- import { LocalIssueProviderCore, buildExternalIssueSnapshotFromLocal, buildLocalIssueConflictReport, createLiveIssueClient, localIssueRoot, parseCommentIdMappings, parseExternalIssueSnapshot, } from './index.js';
15
+ import { sensitiveInputOptions } from '../auth/sensitive-input.js';
16
+ import { LocalIssueProviderCore, buildExternalIssueSnapshotFromLocal, buildLocalIssueConflictReport, createLiveIssueClient, resolveLiveIssueToken, localIssueRoot, parseCommentIdMappings, parseExternalIssueSnapshot, } from './index.js';
16
17
  export async function main(args, cwd = process.cwd()) {
17
18
  const parsed = parseArgs(args);
18
19
  const subcommand = parsed.positional[0];
@@ -218,7 +219,7 @@ async function handleImport(issues, args) {
218
219
  if (from !== "gitea" && from !== "github")
219
220
  throw new Error("Usage: aiwg issue import --from gitea|github (--snapshot-file path | --live --repo owner/name --external-id N)");
220
221
  const snapshot = args.flags.has("live")
221
- ? await createLiveIssueClient({ provider: from, repo: requiredStringFlag(args, "repo"), apiUrl: stringFlag(args, "api-url") }).importIssue(requiredStringFlag(args, "external-id"))
222
+ ? await (await liveClient(args, from)).importIssue(requiredStringFlag(args, "external-id"))
222
223
  : parseExternalIssueSnapshot(JSON.parse(await readRequiredFile(args, "snapshot-file")));
223
224
  if (from !== snapshot.provider)
224
225
  throw new Error(`snapshot provider ${snapshot.provider} does not match --from ${from}`);
@@ -249,7 +250,7 @@ async function handleExport(issues, rest, args) {
249
250
  await writeOptionalOutput(args, snapshot);
250
251
  return;
251
252
  }
252
- const client = createLiveIssueClient({ provider, repo: requiredStringFlag(args, "repo"), apiUrl: stringFlag(args, "api-url") });
253
+ const client = await liveClient(args, provider);
253
254
  if (snapshot.external_id && !args.flags.has("force")) {
254
255
  const remote = await client.fetchIssue(snapshot.external_id);
255
256
  const report = buildLocalIssueConflictReport(issue, remote);
@@ -314,6 +315,11 @@ async function handleIndex(issues, rest, args) {
314
315
  const index = await issues.rebuildIssueIndex({ force: args.flags.has('force') });
315
316
  printJsonOrText(args, index, `Local issue index ready (${index.issues.length} issues)`);
316
317
  }
318
+ async function liveClient(args, provider) {
319
+ const repo = requiredStringFlag(args, 'repo');
320
+ const token = await resolveLiveIssueToken(provider, sensitiveInputOptions(args.raw, 'token'));
321
+ return createLiveIssueClient({ provider, repo, apiUrl: stringFlag(args, 'api-url'), token });
322
+ }
317
323
  function requiredStringFlag(args, flag) {
318
324
  const value = stringFlag(args, flag);
319
325
  if (!value)
@@ -381,7 +387,7 @@ function parseArgs(args) {
381
387
  flags.set(rawKey, true);
382
388
  }
383
389
  }
384
- return { positional, flags };
390
+ return { raw: args, positional, flags };
385
391
  }
386
392
  function stringFlag(args, name) {
387
393
  const value = args.flags.get(name);
@@ -402,10 +408,15 @@ function printUsage() {
402
408
  aiwg issue show <KEY> [--comments last:10|all]
403
409
  aiwg issue import --from gitea|github --snapshot-file path [--json]
404
410
  aiwg issue export <KEY> --to gitea|github [--out path]
411
+ aiwg issue import --from gitea|github --live --repo owner/name --external-id N [token source]
412
+ aiwg issue export <KEY> --to gitea|github --live --repo owner/name [token source]
405
413
  aiwg issue sync conflicts <KEY> --snapshot-file path [--out path]
406
414
  aiwg issue sync map-comments <KEY> --map-file path [--json]
407
415
  aiwg issue comment <KEY> --body "..." [--author name]
408
416
  aiwg issue close <KEY> [--reason "..."]
409
- aiwg issue index rebuild [--force]`);
417
+ aiwg issue index rebuild [--force]
418
+
419
+ Live token sources: --token-interactive, --token-stdin, --token-file <path>,
420
+ --token-store <name>, --token-env <NAME> (see docs/cli/sensitive-values.md).`);
410
421
  }
411
422
  //# sourceMappingURL=cli.js.map
@@ -1,3 +1,5 @@
1
+ import { resolveSensitiveInput } from '../auth/sensitive-input.js';
2
+ import { isInteractive } from '../cli/env.js';
1
3
  export class LiveIssueClient {
2
4
  options;
3
5
  baseUrl;
@@ -63,24 +65,51 @@ export class LiveIssueClient {
63
65
  headers['Content-Type'] = 'application/json';
64
66
  if (this.options.token)
65
67
  headers.Authorization = `Bearer ${this.options.token}`;
66
- const response = await this.fetchFn(`${this.baseUrl}${path}`, {
67
- method,
68
- headers,
69
- body: body === undefined ? undefined : JSON.stringify(body),
70
- });
71
- if (!response.ok) {
72
- const text = await response.text().catch(() => '');
73
- throw new Error(`${this.options.provider} issue API ${method} ${path} failed: ${response.status}${text ? ` ${redactSecrets(text)}` : ''}`);
68
+ try {
69
+ const response = await this.fetchFn(`${this.baseUrl}${path}`, {
70
+ method,
71
+ headers,
72
+ body: body === undefined ? undefined : JSON.stringify(body),
73
+ });
74
+ if (!response.ok) {
75
+ const text = await response.text().catch(() => '');
76
+ throw new Error(`${this.options.provider} issue API ${method} ${path} failed: ${response.status}${text ? ` ${redactSecrets(text, this.options.token)}` : ''}`);
77
+ }
78
+ const result = await response.json();
79
+ // A server can reflect request credentials in issue fields or comments.
80
+ // Scrub them before they reach local storage or CLI output.
81
+ if (!this.options.token)
82
+ return result;
83
+ const encodedToken = JSON.stringify(this.options.token).slice(1, -1);
84
+ return JSON.parse(JSON.stringify(result).split(encodedToken).join('[REDACTED]'));
85
+ }
86
+ catch (error) {
87
+ throw new Error(redactSecrets(error instanceof Error ? error.message : 'Live issue request failed', this.options.token));
74
88
  }
75
- return response.json();
76
89
  }
77
90
  }
78
91
  export function createLiveIssueClient(options) {
79
92
  return new LiveIssueClient({
80
93
  ...options,
81
- token: resolveToken(options.provider, options.tokenEnv ?? process.env),
94
+ token: options.token ?? resolveToken(options.provider, options.tokenEnv ?? process.env),
82
95
  });
83
96
  }
97
+ /** CLI source selection; the synchronous client factory remains available to callers. */
98
+ export async function resolveLiveIssueToken(provider, source = {}, dependencies = {}) {
99
+ const environment = dependencies.environment ?? process.env;
100
+ const isTTY = dependencies.isTTY ?? isInteractive();
101
+ const selected = { ...source, prefix: 'token' };
102
+ const hasSource = selected.interactive || selected.stdin || selected.file !== undefined || selected.store !== undefined || selected.env !== undefined;
103
+ if (!hasSource) {
104
+ // Established behavior: use the provider variable when set, otherwise make
105
+ // unauthenticated requests (public repositories). Prompt only on request.
106
+ const names = provider === 'gitea' ? ['AIWG_GITEA_TOKEN', 'GITEA_TOKEN'] : ['AIWG_GITHUB_TOKEN', 'GITHUB_TOKEN'];
107
+ selected.env = names.find(name => environment[name] !== undefined);
108
+ if (selected.env === undefined)
109
+ return undefined;
110
+ }
111
+ return resolveSensitiveInput(selected, { ...dependencies, environment, isTTY });
112
+ }
84
113
  export function resolveToken(provider, env) {
85
114
  if (provider === 'gitea')
86
115
  return env.AIWG_GITEA_TOKEN ?? env.GITEA_TOKEN;
@@ -148,7 +177,10 @@ function requireString(value, field) {
148
177
  return value;
149
178
  throw new Error(`live issue response missing ${field}`);
150
179
  }
151
- function redactSecrets(value) {
152
- return value.replace(/(token|authorization|password|secret)["':=\s]+[^\s"',}]+/gi, '$1=[REDACTED]');
180
+ function redactSecrets(value, token) {
181
+ const redacted = token ? value.split(token).join('[REDACTED]') : value;
182
+ return redacted
183
+ .replace(/Bearer\s+[^\s"',}]+/gi, 'Bearer [REDACTED]')
184
+ .replace(/(token|authorization|password|secret)["':=\s]+[^\s"',}]+/gi, '$1=[REDACTED]');
153
185
  }
154
186
  //# sourceMappingURL=live.js.map
@@ -9,6 +9,7 @@ import { access, chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile, } from
9
9
  import { existsSync } from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path from 'node:path';
12
+ import { resolveSensitiveInput } from '../auth/sensitive-input.js';
12
13
  import { setPackageEntry } from '../packages/package-registry.js';
13
14
  import { discoverInstallablePackage } from '../packages/package-discovery.js';
14
15
  import { parseTrustRoot, sha256 as artifactSha256, } from '../security/artifact-trust.js';
@@ -236,7 +237,13 @@ export async function verifyIndexedPackage(options) {
236
237
  });
237
238
  return { verification, receipt, entry };
238
239
  }
239
- export async function publishLocalPackage(options) {
240
+ export async function publishLocalPackage(options, sensitiveDependencies = {}) {
241
+ const privateKey = await resolveSensitiveInput({
242
+ ...options.keyInput,
243
+ prefix: 'key',
244
+ file: options.keyInput?.file ?? options.privateKeyPath,
245
+ multiline: true,
246
+ }, sensitiveDependencies);
240
247
  const sourceDir = path.resolve(options.sourceDir);
241
248
  const discovered = await discoverInstallablePackage(sourceDir, options.packageSelector);
242
249
  const unsigned = await createProvenanceEnvelope({
@@ -248,12 +255,17 @@ export async function publishLocalPackage(options) {
248
255
  publisher: options.publisher,
249
256
  sequence: options.sequence,
250
257
  });
251
- const privateKey = await readFile(path.resolve(options.privateKeyPath), 'utf8');
252
258
  const publicKey = options.publicKeyPath ? await readFile(path.resolve(options.publicKeyPath), 'utf8') : undefined;
253
- const envelope = signProvenanceEnvelope(unsigned, privateKey, {
254
- keyId: options.keyId,
255
- publicKeyPem: publicKey,
256
- });
259
+ let envelope;
260
+ try {
261
+ envelope = signProvenanceEnvelope(unsigned, privateKey, {
262
+ keyId: options.keyId,
263
+ publicKeyPem: publicKey,
264
+ });
265
+ }
266
+ catch {
267
+ throw new Error('Marketplace signing failed; check the Ed25519 private key and public-key options.');
268
+ }
257
269
  const lock = createPackageLock(envelope, envelope.publication.publishedAt);
258
270
  const fortemi = await buildFortemiEnvelopeShard(envelope);
259
271
  const receipt = createOperationReceipt({