@kici-dev/orchestrator 0.9.1 → 0.10.0

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 (88) hide show
  1. package/dist/__test-helpers__/test-db.d.ts +14 -1
  2. package/dist/agent/dispatcher.d.ts +137 -41
  3. package/dist/agent/registry.d.ts +79 -2
  4. package/dist/app.d.ts +2 -0
  5. package/dist/cancel/cancel-held-run.d.ts +68 -0
  6. package/dist/cancel/cancel-route-answer.d.ts +21 -0
  7. package/dist/cancel/cancel-run.d.ts +32 -0
  8. package/dist/cli/api-client.d.ts +3 -0
  9. package/dist/cli/commands/context.d.ts +3 -1
  10. package/dist/cli/commands/runs.d.ts +7 -0
  11. package/dist/cli/commands/shared/unbound-context-warning.d.ts +43 -0
  12. package/dist/cli/commands/signing-key.d.ts +22 -1
  13. package/dist/cli.js +1273 -715
  14. package/dist/cluster/cluster-settings-reader.d.ts +1 -1
  15. package/dist/cluster/coordinator.d.ts +2 -0
  16. package/dist/cluster/live-coordinator-peer.d.ts +13 -0
  17. package/dist/config.d.ts +9 -0
  18. package/dist/contexts/context-store.d.ts +3 -2
  19. package/dist/contexts/protection/satisfiability.d.ts +6 -3
  20. package/dist/db/migrations/145_execution_runs_workflow_provenance.d.ts +4 -0
  21. package/dist/db/migrations/146_pending_job_contexts_context_resolution.d.ts +4 -0
  22. package/dist/db/migrations/147_sealed_job_secrets.d.ts +19 -0
  23. package/dist/db/migrations/148_cluster_settings_sealed_secrets_retry_backoff_ms.d.ts +21 -0
  24. package/dist/db/migrations/149_workflow_registrations_dep_cache_key.d.ts +4 -0
  25. package/dist/db/migrations/150_execution_runs_status_epoch.d.ts +18 -0
  26. package/dist/db/migrations/151_workflow_registrations_dep_cache_key_sha.d.ts +4 -0
  27. package/dist/db/repos/signing-keys-repo.d.ts +48 -3
  28. package/dist/db/types.d.ts +81 -1
  29. package/dist/git/clone-token-auth.d.ts +7 -0
  30. package/dist/git/dispatch-git-auth.d.ts +63 -0
  31. package/dist/lockfile-validate.d.ts +4 -2
  32. package/dist/metrics/prometheus.d.ts +1 -1
  33. package/dist/oidc/id-token-claims.d.ts +10 -0
  34. package/dist/oidc/oidc-mint-registration.d.ts +2 -0
  35. package/dist/oidc/orchestrator-mint.d.ts +25 -4
  36. package/dist/oidc/reconcile-signing-key.d.ts +31 -6
  37. package/dist/oidc/resolve-signer.d.ts +12 -6
  38. package/dist/pipeline/decorating-secret-resolver.d.ts +5 -3
  39. package/dist/pipeline/dispatch-matched-workflow.d.ts +105 -13
  40. package/dist/pipeline/dispatch-matched-workflow.test-helpers.d.ts +121 -0
  41. package/dist/pipeline/global-dispatch-identity.d.ts +76 -0
  42. package/dist/pipeline/global-dispatch.d.ts +91 -0
  43. package/dist/pipeline/global-eval-round.d.ts +56 -4
  44. package/dist/pipeline/global-round-hold.d.ts +41 -0
  45. package/dist/pipeline/held-context-data.d.ts +143 -0
  46. package/dist/pipeline/held-round-registrations.d.ts +40 -0
  47. package/dist/pipeline/install-gate.d.ts +93 -0
  48. package/dist/pipeline/install-secrets-resolver.d.ts +28 -13
  49. package/dist/pipeline/job-contexts.d.ts +21 -6
  50. package/dist/pipeline/pending-workflow-context.d.ts +15 -3
  51. package/dist/pipeline/process-webhook.d.ts +38 -1
  52. package/dist/pipeline/processor.d.ts +20 -0
  53. package/dist/pipeline/rerun-global.d.ts +28 -0
  54. package/dist/pipeline/rerun-inputs.d.ts +88 -0
  55. package/dist/pipeline/rerun.d.ts +25 -2
  56. package/dist/pipeline/resume-workflow.d.ts +36 -2
  57. package/dist/pipeline/unbound-job-context.d.ts +34 -0
  58. package/dist/pipeline/workflow-lock-at-sha.d.ts +67 -0
  59. package/dist/provenance/backfill-run.d.ts +5 -0
  60. package/dist/providers/github/check-status-poster.d.ts +4 -16
  61. package/dist/queue/job-queue.d.ts +127 -10
  62. package/dist/queue/terminalize-unroutable.d.ts +37 -4
  63. package/dist/registration/registration-index.d.ts +13 -1
  64. package/dist/registration/registration-store.d.ts +10 -1
  65. package/dist/reporting/execution-tracker.d.ts +149 -8
  66. package/dist/reporting/log-chunk-sink.d.ts +1 -1
  67. package/dist/reporting/log-writer.d.ts +14 -0
  68. package/dist/reporting/step-log-path.d.ts +8 -0
  69. package/dist/reporting/step-log-reader.d.ts +21 -3
  70. package/dist/routes/admin-signing-keys.d.ts +33 -0
  71. package/dist/scaler/agent-fit.d.ts +159 -0
  72. package/dist/scaler/container-routing.d.ts +16 -16
  73. package/dist/scaler/manager.d.ts +23 -17
  74. package/dist/secrets/context-secret-resolver.d.ts +1 -1
  75. package/dist/secrets/index.d.ts +1 -1
  76. package/dist/secrets/job-secret-gate.d.ts +25 -3
  77. package/dist/secrets/job-secret-seal.d.ts +36 -0
  78. package/dist/secrets/master-key-rotation.d.ts +7 -3
  79. package/dist/secrets/secret-resolver.d.ts +42 -23
  80. package/dist/server.js +27690 -23447
  81. package/dist/standalone.js +58162 -53103
  82. package/dist/worker/in-memory-job-queue.d.ts +12 -3
  83. package/dist/worker/reroute-dispatch.d.ts +35 -0
  84. package/dist/ws/agent-handler.d.ts +23 -10
  85. package/dist/ws/git-credential-relay.d.ts +16 -3
  86. package/installer-image-digests.json +3 -3
  87. package/package.json +4 -4
  88. package/sbom.spdx.json +53 -53
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Capability label for hosts that can run containers.
2
+ * Label for hosts that can run containers.
3
3
  *
4
- * An operator may put this on a pool's label sets and on a container job's
5
- * `runsOn` to steer container work at hosts with a runtime. KiCI does NOT add
6
- * it automatically, and does not gate routing on it.
4
+ * The agent reports it itself at registration when it finds a Docker socket,
5
+ * alongside `kici:runtime:podman` for a Podman one and the `kici:os:*` /
6
+ * `kici:arch:*` facts: whether a machine has a container runtime is that
7
+ * machine's fact, not the orchestrator's. Probing the orchestrator's own
8
+ * filesystem answers a different question — it was tried, and it stranded
9
+ * container jobs that had been running fine, because the probe and the job ran
10
+ * in different places.
7
11
  *
8
- * **Why it is not automatic.** A container job needs a runtime on the host that
9
- * ends up running it, and the orchestrator does not know that: an agent runs on
10
- * its own machine, and whether that machine has docker or podman is the agent's
11
- * fact, not the orchestrator's. Probing the orchestrator's own filesystem
12
- * answers a different question — it was tried, and it stranded container jobs
13
- * that had been running fine, because the probe and the job ran in different
14
- * places.
12
+ * Routing gates on the reported label (`canAgentRunJob`): a `container:` job
13
+ * reaches an agent only when that agent was started for it, or reports
14
+ * `kici:runtime:docker` or `kici:runtime:podman`. For an operator's own agent
15
+ * the gate applies from the release that reports runtime labels at all; an
16
+ * older agent reports none, so its silence proves nothing and a job it cannot
17
+ * run fails on it with a message that names the missing runtime.
15
18
  *
16
- * Doing this properly means the AGENT reporting the capability at registration,
17
- * alongside the `kici:os:*` / `kici:arch:*` facts it already self-reports. Until
18
- * then a mis-routed container job fails at the image preflight or a backend's
19
- * fail-fast, both of which name what is missing — a clear late error, rather
20
- * than a job that silently matches nothing and never runs.
19
+ * An operator may still put the label on a pool's label sets and on a container
20
+ * job's `runsOn` to steer container work at hosts with a runtime.
21
21
  */
22
22
  export declare const KICI_RUNTIME_DOCKER_LABEL = "kici:runtime:docker";
23
23
  //# sourceMappingURL=container-routing.d.ts.map
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { ResourceRequest, ScalerCapacitySummary } from '@kici-dev/engine';
10
10
  import { ScaleDownReason } from './scaler-events.js';
11
+ import { type ScalerAgentView } from './agent-fit.js';
11
12
  import type { ScalerEventEmitterLike } from './event-backend.js';
12
13
  import type { ClaimStore, ClaimedCredentials } from './claim-store.js';
13
14
  import type { BackendFailureSummary } from './failure-tracker.js';
@@ -374,6 +375,14 @@ export declare class ScalerManager {
374
375
  * dispatch has not yet arrived.
375
376
  */
376
377
  private readonly warmAgents;
378
+ /**
379
+ * Registered agents this instance started for a specific queued job, keyed
380
+ * by agent id. The spawning entry that carries the binding is deleted at
381
+ * registration, so this is where {@link agentView} reads it: the agent always
382
+ * runs its own job, and an agent started inside a job's image runs nothing
383
+ * else.
384
+ */
385
+ private readonly boundAgents;
377
386
  /**
378
387
  * Warm agents whose destroy is issued but has not settled yet.
379
388
  *
@@ -497,25 +506,25 @@ export declare class ScalerManager {
497
506
  */
498
507
  private warmPoolLabelSetFillable;
499
508
  /**
500
- * Whether this scaler pre-spawned (warm-filled) the agent, i.e. whether
501
- * {@link canPrespawnedAgentServe} can ever answer false for it. A caller that
502
- * picks one agent at a time uses this to decide whether the suitability check
503
- * is worth carrying at all.
509
+ * Whether this scaler pre-spawned (warm-filled) the agent.
504
510
  */
505
511
  isPrespawnedAgent(agentId: string): boolean;
506
512
  /**
507
- * Whether a pre-spawned (warm) agent can serve this job.
513
+ * What this scaler knows about an agent it started, for `canAgentRunJob`,
514
+ * or undefined for an agent it did not start.
515
+ */
516
+ agentView(agentId: string): ScalerAgentView | undefined;
517
+ /**
518
+ * Whether a pre-spawned (warm) agent's shape fits the shape a job declares.
508
519
  *
509
520
  * A warm agent is generic by construction: it was started before the job
510
- * existed, at the pool's declared shape and running the pool's agent image.
511
- * Both are applied when the agent starts and cannot be changed afterwards —
512
- * nothing in the agent reads a job's `resources`, and an already-running
513
- * container cannot become a different image. So a job that needs something
514
- * else must get its own agent instead of silently running with the pool's.
521
+ * existed, at the pool's declared shape. The shape is applied when the agent
522
+ * starts and cannot be changed afterwards — nothing in the agent reads a
523
+ * job's `resources`. So a job that needs another shape must get its own
524
+ * agent instead of silently running with the pool's.
515
525
  *
516
- * Returns true for any agent this scaler did not pre-spawn: a job-bound or
517
- * static agent is not this predicate's business. {@link isPrespawnedAgent}
518
- * answers that half on its own.
526
+ * Returns true for any agent this scaler did not pre-spawn: a job-bound agent
527
+ * was started at its job's shape.
519
528
  *
520
529
  * Only the fields the job actually declares are compared. `resolveEffective`
521
530
  * layers a job's declaration over the label set and the scaler defaults field
@@ -536,10 +545,7 @@ export declare class ScalerManager {
536
545
  * them apart, so a job matched on `requests` alone can still land on an agent
537
546
  * capped somewhere else entirely.
538
547
  */
539
- canPrespawnedAgentServe(agentId: string, job: {
540
- resources?: ResourceRequest;
541
- hasOwnContainerImage: boolean;
542
- }): boolean;
548
+ private prespawnedShapeFits;
543
549
  /**
544
550
  * Whether one side (`requests` or `limits`) of a job's declared shape leaves
545
551
  * the agent's resolved side unchanged.
@@ -22,7 +22,7 @@ export interface ContextSecretResolverDeps {
22
22
  logger: Logger;
23
23
  }
24
24
  /**
25
- * Build a `SecretResolver` over the org's contexts, bindings, and registered
25
+ * Build a `SecretResolver` over the org's context bindings and registered
26
26
  * secret backends. The `pg` backend decrypts through the master key; external
27
27
  * backends (Vault) return plaintext, so their decrypt is identity.
28
28
  */
@@ -11,7 +11,7 @@ export { secretOutputAad, sealSecretOutput, unsealSecretOutput } from './secret-
11
11
  export { rotateMasterKeyWrappedTables, selfHealStrandedDashboardKey, selfHealStrandedSigningKey, strandedKeyError, type MasterKeyRotationResult, type SweepResult, } from './master-key-rotation.js';
12
12
  export { encrypt, decrypt, deriveKey, generateMasterKey, type EncryptedValue, } from '@kici-dev/shared';
13
13
  export { VaultSecretStore, type VaultConfig } from './vault-secret-store.js';
14
- export { SecretResolver, type SecretResolverDeps, type ResolvedSecretMeta, type ContextStoreLike, type BindingStoreLike, type SecretStoreLike, } from './secret-resolver.js';
14
+ export { SecretResolver, type SecretResolverDeps, type ResolvedSecretMeta, type MatchedContextRef, type BindingStoreLike, type SecretStoreLike, } from './secret-resolver.js';
15
15
  export { BackendSyncManager } from './backend-sync.js';
16
16
  export { RbacEnforcer, PermissionDeniedError, type Role, type Permission } from './rbac.js';
17
17
  export { TokenManager } from './token-manager.js';
@@ -8,8 +8,8 @@
8
8
  * `docs/user/container-jobs.md` do, so a bound-only rule would refuse every
9
9
  * documented use.
10
10
  *
11
- * Resolving outside the bindings is not the same as resolving with no
12
- * authorization. Three independent checks stand between a job and the value,
11
+ * Resolving outside the job's `contexts:` list is not the same as resolving with
12
+ * no authorization. Three independent checks stand between a job and the value,
13
13
  * and this module is where all three run:
14
14
  *
15
15
  * 1. the reserved namespace is refused — a job may never name the
@@ -20,6 +20,14 @@
20
20
  * the branch restriction, `minimumTrust`, concurrency, reviewers and wait
21
21
  * timers the operator configured on `prod` apply to a `prod:` reference.
22
22
  *
23
+ * The value is then read through the secret-scope bindings of the context row
24
+ * those rules ran against — for a name a glob context matches, the glob row —
25
+ * the way a job's bound context secrets resolve. One deprecated fallback remains
26
+ * (removal planned for v1.0.0): when the row matched by the exact name is not a
27
+ * glob context and no scope bound to it carries the key, the scope named after
28
+ * the context is read instead, and a deprecation warning is logged. A
29
+ * glob-matched row never reads a same-named scope.
30
+ *
23
31
  * The lock declaration is the fourth check and lives at the callers: the relay
24
32
  * pins the wire ref to `execution_jobs.git_credentials`, and container-registry
25
33
  * refs come from the lock by construction.
@@ -27,7 +35,7 @@
27
35
  * Callers use this instead of `SecretResolverApi.resolveNamedInternal`, which
28
36
  * is the system-scoped direct lookup and applies none of the above.
29
37
  */
30
- import type { TrustTier } from '@kici-dev/engine';
38
+ import { type TrustTier } from '@kici-dev/engine';
31
39
  import type { ContextStore } from '../contexts/context-store.js';
32
40
  import { type JobDispatchContext } from '../contexts/protection/pipeline.js';
33
41
  import type { SecretResolverApi } from './secret-resolver.js';
@@ -37,6 +45,15 @@ import type { SecretResolverApi } from './secret-resolver.js';
37
45
  * its `customer_id`, so a job can never legitimately present this value.
38
46
  */
39
47
  export declare const RESERVED_ORG_ID = "__system__";
48
+ /**
49
+ * A refusal this module decides — a reserved namespace, an untrusted tier, a
50
+ * context that does not exist or does not admit the run, a secret that is not
51
+ * there. Distinct from a store or resolver failure, which propagates as-is, so
52
+ * a caller can tell "this job may not have it" from "we could not look".
53
+ */
54
+ export declare class JobSecretRefusedError extends Error {
55
+ constructor(message: string);
56
+ }
40
57
  /**
41
58
  * Refuse a reference into the orchestrator's own reserved namespace.
42
59
  *
@@ -64,6 +81,11 @@ export interface ResolveJobQualifiedSecretArgs {
64
81
  /** The dispatch facts the protection rules evaluate against, from server truth. */
65
82
  dispatchCtx: JobDispatchContext;
66
83
  trustTier: TrustTier | undefined;
84
+ /**
85
+ * Ids of contexts whose protection rules already admitted this job. Their
86
+ * rules are not re-run; the namespace and trust-tier refusals still apply.
87
+ */
88
+ admittedContextIds?: ReadonlySet<string>;
67
89
  }
68
90
  /**
69
91
  * Resolve one `<context>:<key>` reference on behalf of a running job.
@@ -0,0 +1,36 @@
1
+ import type { ResolvedMasterKeys } from './config.js';
2
+ /** The job-config fields that carry secret material. */
3
+ export declare const SEALED_JOB_CONFIG_KEYS: readonly ['secrets', 'namespacedSecrets', 'installEnvSecrets', 'containerRegistryAuth', 'npmRegistries', 'orchestratorPrivateKey'];
4
+ /** The key version every seal is written at. The AAD, not the version, binds a value. */
5
+ export declare const JOB_SECRETS_KEY_VERSION = 1;
6
+ /**
7
+ * Thrown when a sealed value cannot be opened: it was sealed with a master key
8
+ * this orchestrator does not hold. During a rolling key rotation that is a
9
+ * coordinator not yet restarted with the new key; the message names the fix.
10
+ */
11
+ export declare class JobSecretsUnsealError extends Error {
12
+ constructor(runId: string, cause: string);
13
+ }
14
+ /**
15
+ * Register the master keys the stores seal with. `null` keeps the fields in
16
+ * plaintext and logs the one warning that names the setting.
17
+ */
18
+ export declare function configureJobSecretSealing(keys: ResolvedMasterKeys | null): void;
19
+ /** The AAD a stored job's sealed secrets are bound to. */
20
+ export declare function jobSecretsAad(runId: string): string;
21
+ /** Seal `value` for `runId`; `null` when no master key is configured. */
22
+ export declare function sealJobSecretValue(runId: string, value: unknown): string | null;
23
+ /** Open a value {@link sealJobSecretValue} sealed, with the old key as fallback. */
24
+ export declare function unsealJobSecretValue(runId: string, sealed: string): unknown;
25
+ /**
26
+ * Split a job config into what is stored in the plain column and the sealed
27
+ * secret fields. With nothing to seal, or no master key, the config is
28
+ * returned as is and `sealed` is null.
29
+ */
30
+ export declare function sealJobConfig(runId: string, jobConfig: Record<string, unknown>): {
31
+ jobConfig: Record<string, unknown>;
32
+ sealed: string | null;
33
+ };
34
+ /** The job config with its sealed secret fields merged back. A null `sealed` returns it as is. */
35
+ export declare function unsealJobConfig(runId: string, jobConfig: Record<string, unknown>, sealed: string | null | undefined): Record<string, unknown>;
36
+ //# sourceMappingURL=job-secret-seal.d.ts.map
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Master-key rotation sweeps for the four wrapped stores `rotate-key` did not
2
+ * Master-key rotation sweeps for the wrapped stores `rotate-key` did not
3
3
  * cover: the provenance signing key, the dashboard-encryption key, run
4
- * ephemeral keys, and stored secret outputs.
4
+ * ephemeral keys, stored secret outputs, and the sealed secrets of stored jobs.
5
5
  *
6
6
  * Each sweep runs in its own transaction with the same skip-and-count
7
7
  * discipline as `BackendRegistry.rotateKey` — a row neither key opens is
@@ -26,10 +26,14 @@ export interface MasterKeyRotationResult {
26
26
  dashboardKeys: SweepResult;
27
27
  ephemeralKeys: SweepResult;
28
28
  secretOutputs: SweepResult;
29
+ /** `sealed_secrets` of `dispatch_queue`, `pending_job_contexts` and `pending_workflow_contexts`. */
30
+ jobSecrets: SweepResult;
29
31
  }
30
32
  type Warn = (message: string, meta: Record<string, unknown>) => void;
33
+ /** Rows of `dispatch_queue` one rotation transaction re-seals. */
34
+ export declare const JOB_SECRETS_SWEEP_BATCH = 500;
31
35
  /**
32
- * Run all four sweeps, each in its own transaction so a bug in one cannot roll
36
+ * Run all the sweeps, each in its own transaction so a bug in one cannot roll
33
37
  * back a good rotation of another. Order is least-to-most volume, so the
34
38
  * singleton key tables — the two whose loss brings the orchestrator down — move
35
39
  * first.
@@ -2,9 +2,10 @@
2
2
  * Secret resolver for dispatch-time secret resolution.
3
3
  *
4
4
  * Uses context bindings + scope resolver to match secrets from multiple backends.
5
- * When a job dispatches with a context name, the resolver:
6
- * 1. Looks up the context by name
7
- * 2. Gets bindings for that context
5
+ * When a job dispatches through a context, the resolver:
6
+ * 1. Takes the context row the dispatch path already matched (a fixed context
7
+ * by exact name, or a glob context whose pattern matched the declared name)
8
+ * 2. Gets bindings for that row
8
9
  * 3. Queries ALL registered backend stores for secrets
9
10
  * 4. Prefixes each secret's scope with the backend name (e.g., pg:aws/prod)
10
11
  * 5. Uses resolveSecretsWithProvenance to match bindings against prefixed secrets
@@ -19,14 +20,23 @@ import { type ContextBinding, type HostFacts, type ScopedSecret } from '@kici-de
19
20
  import type { Logger } from '@kici-dev/shared';
20
21
  import type { AuditLogger } from './audit-logger.js';
21
22
  /**
22
- * Minimal context store interface (subset needed by resolver).
23
+ * The context a job's secrets resolve through.
24
+ *
25
+ * `id` is the row the dispatch path matched — which, for a glob context, is not
26
+ * the row whose name equals the declared one — so the resolver reads that row's
27
+ * bindings and never matches a name again: a second match could land on a
28
+ * different row than the one the caller's protection rules and held-run
29
+ * checks ran against. `name` is the name the job declared; it labels the audit
30
+ * entry and keys per-context namespacing.
23
31
  */
24
- export interface ContextStoreLike {
25
- getByName(orgId: string, name: string): Promise<{
26
- id: string;
27
- name: string;
28
- orgId: string;
29
- } | null>;
32
+ export interface MatchedContextRef {
33
+ id: string;
34
+ name: string;
35
+ }
36
+ /** The run and job a resolution is recorded against in the secret audit log. */
37
+ export interface SecretResolutionAttribution {
38
+ runId?: string;
39
+ jobId?: string;
30
40
  }
31
41
  /**
32
42
  * Minimal binding store interface (subset needed by resolver).
@@ -49,7 +59,6 @@ export interface SecretStoreLike {
49
59
  * Dependencies for the SecretResolver.
50
60
  */
51
61
  export interface SecretResolverDeps {
52
- contextStore: ContextStoreLike;
53
62
  bindingStore: BindingStoreLike;
54
63
  /** Map of backend name to store. Replaces single secretStore. */
55
64
  backendStores: Map<string, SecretStoreLike>;
@@ -69,7 +78,7 @@ export interface ResolvedSecretMeta {
69
78
  * can flow through `ProcessingDeps.secretResolver`.
70
79
  */
71
80
  export interface SecretResolverApi {
72
- resolveForJob(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
81
+ resolveForContext(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts, attribution?: SecretResolutionAttribution): Promise<Record<string, string>>;
73
82
  /**
74
83
  * System-scoped direct lookup — no context binding, no protection rule, no
75
84
  * trust tier. A job-originated reference goes through
@@ -80,7 +89,12 @@ export interface SecretResolverApi {
80
89
  runId?: string;
81
90
  jobId?: string;
82
91
  }): Promise<string | null>;
83
- resolveForJobWithMeta(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
92
+ resolveForContextWithMeta(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
93
+ /**
94
+ * How many scope bindings the context row `contextId` has. A context with
95
+ * none resolves no secret; the dispatch path reads this to say so in its log.
96
+ */
97
+ countContextBindings?(contextId: string): Promise<number>;
84
98
  }
85
99
  /**
86
100
  * Resolves secrets for a job by matching context bindings against scoped secrets
@@ -92,7 +106,6 @@ export interface SecretResolverApi {
92
106
  * stripping the backend prefix.
93
107
  */
94
108
  export declare class SecretResolver implements SecretResolverApi {
95
- private readonly contextStore;
96
109
  private readonly bindingStore;
97
110
  private readonly backendStores;
98
111
  private readonly auditLogger;
@@ -102,14 +115,16 @@ export declare class SecretResolver implements SecretResolverApi {
102
115
  * Resolve secrets for a job dispatch.
103
116
  *
104
117
  * @param orgId - Organization ID
105
- * @param contextName - Context name to resolve secrets for
118
+ * @param context - The matched context row and the name the job declared
106
119
  * @param hostCtx - Optional fan-out child identity for per-host resolution.
107
120
  * When supplied, each binding is gated by its `host_pattern` and its
108
121
  * `scope_pattern` is templated per-child; when omitted, only fleet-wide
109
122
  * (`'**'`) non-templated bindings contribute.
123
+ * @param attribution - The run and job the audit entry names, when the
124
+ * caller resolves on behalf of one.
110
125
  * @returns Flat map of decrypted secret key-value pairs
111
126
  */
112
- resolveForJob(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, string>>;
127
+ resolveForContext(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts, attribution?: SecretResolutionAttribution): Promise<Record<string, string>>;
113
128
  /**
114
129
  * Resolve a single named secret by (orgId, scope, key), optionally scoped to
115
130
  * a specific backend.
@@ -122,18 +137,20 @@ export declare class SecretResolver implements SecretResolverApi {
122
137
  *
123
138
  * A JOB-ORIGINATED reference goes through `resolveJobQualifiedSecret`
124
139
  * (`secrets/job-secret-gate.ts`) instead, which runs the named context's
125
- * protection rules and the trust-tier strip before reaching this method. The
126
- * `Internal` suffix is the enforcement: this method's doc comment already
127
- * warned that it bypassed context bindings, and three call sites took it
128
- * anyway — a warning is not a boundary, but a name shows up in a grep of
129
- * callers.
140
+ * protection rules and the trust-tier strip, then reads the value through
141
+ * that context's bindings with `resolveForContext`. It calls this method in
142
+ * one deprecated case only, after its own checks pass: a non-glob context
143
+ * matched by its exact name, whose bound scopes do not carry the key, reads
144
+ * the scope named after the context (removal planned for v1.0.0). The `Internal` suffix marks the
145
+ * boundary: a warning in a doc comment is not one, but a name shows up in a
146
+ * grep of callers.
130
147
  *
131
148
  * When `store` is omitted, backends are tried in Map iteration order (the
132
149
  * order they were registered) and the first hit wins. An explicit `store`
133
150
  * restricts the lookup to that one backend and returns null on miss.
134
151
  *
135
152
  * Audit-log: writes one `resolve_named` entry on success. Throws when the
136
- * named store is requested but doesn't exist, mirroring `resolveForJob`'s
153
+ * named store is requested but doesn't exist, mirroring `resolveForContext`'s
137
154
  * fail-fast policy — the caller asked for a specific backend and
138
155
  * it's gone.
139
156
  */
@@ -142,12 +159,14 @@ export declare class SecretResolver implements SecretResolverApi {
142
159
  runId?: string;
143
160
  jobId?: string;
144
161
  }): Promise<string | null>;
162
+ /** How many scope bindings the context row `contextId` has. */
163
+ countContextBindings(contextId: string): Promise<number>;
145
164
  /**
146
165
  * Resolve secrets with metadata (per, for secrets.getMeta).
147
166
  *
148
167
  * Returns the secret value along with which backend and scope provided it.
149
168
  */
150
- resolveForJobWithMeta(orgId: string, contextName: string, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
169
+ resolveForContextWithMeta(orgId: string, context: MatchedContextRef, hostCtx?: HostFacts): Promise<Record<string, ResolvedSecretMeta>>;
151
170
  /**
152
171
  * Collect all secrets from all backend stores, prefixing scopes.
153
172
  * Per /: unreachable backends are tracked but not fatal here.