@kici-dev/orchestrator 0.16.0 → 0.17.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.
@@ -1,8 +1,19 @@
1
- import { ApprovalDecision } from '@kici-dev/engine';
1
+ import { AccessLogAction, ApprovalDecision } from '@kici-dev/engine';
2
2
  import type { HeldRun } from '../db/types.js';
3
3
  import { HoldOutcome } from '../pipeline/security-hold-check.js';
4
4
  import type { HeldRunStore, ReleaseSignal } from '../contexts/held-runs.js';
5
5
  import { type TeamMembershipLookup } from './approval-resolver.js';
6
+ /**
7
+ * The access-log action a held-run decision is audited under.
8
+ *
9
+ * A `kici run --approve-all` breakglass approval audits as
10
+ * `held_run.auto_approve`, so the trail shows "auto-approved by the dispatcher"
11
+ * apart from an interactive approve. The marker only renames the record:
12
+ * eligibility is enforced the same way for both. It never applies to a reject.
13
+ * Every approving surface takes its action from here, so the Platform-relayed
14
+ * and the direct admin-route approvals cannot audit the same act two ways.
15
+ */
16
+ export declare function heldRunDecisionAction(decision: ApprovalDecision, autoApprove: boolean): AccessLogAction;
6
17
  /** Whether a decision's post-commit consequence ran to completion. */
7
18
  export interface DecisionConsequenceResult {
8
19
  /** True when the whole consequence chain ran; false when a step of it threw. */
@@ -469,6 +469,10 @@ export declare class AdminApiClient {
469
469
  vmIds: string[];
470
470
  timeoutMs?: number;
471
471
  }): Promise<ScalerOrphansStopResponse>;
472
+ /**
473
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
474
+ * (`kici-admin token create`). Removed in v1.0.0.
475
+ */
472
476
  createApiKey(opts: {
473
477
  label?: string;
474
478
  routingKeys?: string[];
@@ -477,6 +481,10 @@ export declare class AdminApiClient {
477
481
  key: string;
478
482
  routingKeys: string[];
479
483
  }>;
484
+ /**
485
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
486
+ * with `routingKey` (`kici-admin token create --routing-key`). Removed in v1.0.0.
487
+ */
480
488
  addRoutingKeyPermission(keyId: string, pattern: string): Promise<{
481
489
  id: string;
482
490
  pattern: string;
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Platform API key management commands for kici-admin.
2
+ * `kici-admin api-key` — deprecated.
3
3
  *
4
- * These commands target the Platform admin API for managing API keys
5
- * and routing key permissions. Uses the same --url flag (point to Platform URL).
4
+ * The group targeted `/api/v1/api-keys`, which no KiCI server serves, so it
5
+ * never worked. Both subcommands stay registered until v1.0.0 and exit with a
6
+ * message that points at `kici-admin token create`, the per-person
7
+ * orchestrator token a developer passes to `kici connect`.
6
8
  *
7
9
  * api-key create, add-routing-key
8
10
  */
9
11
  import type { Command } from 'commander';
10
12
  import type { AdminApiClient } from '../api-client.js';
11
- export declare function registerApiKeyCommands(program: Command, getClient: () => AdminApiClient): void;
13
+ export declare const API_KEY_DEPRECATED_MESSAGE: string;
14
+ /**
15
+ * Register the deprecated `api-key` group. `getClient` is accepted for the
16
+ * registration signature every command group shares and is never called.
17
+ */
18
+ export declare function registerApiKeyCommands(program: Command, _getClient: () => AdminApiClient): void;
12
19
  //# sourceMappingURL=api-key.d.ts.map
package/dist/cli.js CHANGED
@@ -517,9 +517,17 @@ var AdminApiClient = class {
517
517
  async stopScalerOrphans(body) {
518
518
  return this.request("POST", "/api/v1/admin/scaler/orphans/stop", body);
519
519
  }
520
+ /**
521
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
522
+ * (`kici-admin token create`). Removed in v1.0.0.
523
+ */
520
524
  async createApiKey(opts) {
521
525
  return this.request("POST", "/api/v1/api-keys", opts);
522
526
  }
527
+ /**
528
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
529
+ * with `routingKey` (`kici-admin token create --routing-key`). Removed in v1.0.0.
530
+ */
523
531
  async addRoutingKeyPermission(keyId, pattern) {
524
532
  return this.request("POST", `/api/v1/api-keys/${encodeURIComponent(keyId)}/routing-permissions`, { pattern });
525
533
  }
@@ -1364,26 +1372,19 @@ function registerAgentCommands(program, getClient) {
1364
1372
  }
1365
1373
  //#endregion
1366
1374
  //#region src/cli/commands/api-key.ts
1367
- function registerApiKeyCommands(program, getClient) {
1368
- const apiKey = program.command("api-key").description("Manage Platform API keys and routing keys");
1369
- apiKey.command("create").description("Create a new API key with optional routing key permissions").option("--label <label>", "Label for the API key", "unnamed").option("--routing-keys <keys>", "Comma-separated routing key patterns (e.g. github:42,github:99)").action(cliAction(async (opts) => {
1370
- const routingKeys = opts.routingKeys ? opts.routingKeys.split(",").map((k) => k.trim()) : void 0;
1371
- const result = await getClient().createApiKey({
1372
- label: opts.label,
1373
- routingKeys
1374
- });
1375
- console.log(`API key created successfully.`);
1376
- console.log(`Key ID: ${result.id}`);
1377
- console.log(`Key: ${result.key}`);
1378
- if (result.routingKeys?.length) console.log(`Routing keys: ${result.routingKeys.join(", ")}`);
1379
- console.log("");
1380
- console.log("WARNING: Save this key now -- it cannot be recovered after this point.");
1375
+ const API_KEY_DEPRECATED_MESSAGE = "kici-admin api-key is deprecated and has no effect: no KiCI server serves /api/v1/api-keys. Mint a per-person orchestrator token instead: kici-admin token create <label> --role admin --subject <who> --expires <duration>. This command is removed in v1.0.0.";
1376
+ const DEPRECATED_DESCRIPTION = "Deprecated: use kici-admin token create (removed in v1.0.0)";
1377
+ /**
1378
+ * Register the deprecated `api-key` group. `getClient` is accepted for the
1379
+ * registration signature every command group shares and is never called.
1380
+ */
1381
+ function registerApiKeyCommands(program, _getClient) {
1382
+ const apiKey = program.command("api-key").description(DEPRECATED_DESCRIPTION);
1383
+ apiKey.command("create").description(DEPRECATED_DESCRIPTION).option("--label <label>", "Ignored", "unnamed").option("--routing-keys <keys>", "Ignored").action(cliAction(async () => {
1384
+ throw new Error(API_KEY_DEPRECATED_MESSAGE);
1381
1385
  }));
1382
- apiKey.command("add-routing-key <id> <pattern>").description("Add a routing key permission pattern to an API key").action(cliAction(async (id, pattern) => {
1383
- const result = await getClient().addRoutingKeyPermission(id, pattern);
1384
- console.log(`Routing key permission added.`);
1385
- console.log(`Permission ID: ${result.id}`);
1386
- console.log(`Pattern: ${result.pattern}`);
1386
+ apiKey.command("add-routing-key <id> <pattern>").description(DEPRECATED_DESCRIPTION).action(cliAction(async () => {
1387
+ throw new Error(API_KEY_DEPRECATED_MESSAGE);
1387
1388
  }));
1388
1389
  }
1389
1390
  //#endregion
@@ -22078,7 +22079,7 @@ var S3CacheStorage = class {
22078
22079
  client;
22079
22080
  /** Separate client for pre-signed URL generation (uses externalEndpoint if configured). */
22080
22081
  presignClient;
22081
- /** Separate client for the host CLI's pre-signed upload URL (uses uploadEndpoint if configured). */
22082
+ /** Client for the developer upload URL: uploadEndpoint, else the presign client. */
22082
22083
  uploadPresignClient;
22083
22084
  bucket;
22084
22085
  prefix;
@@ -22098,11 +22099,10 @@ var S3CacheStorage = class {
22098
22099
  endpoint: options.externalEndpoint
22099
22100
  });
22100
22101
  else this.presignClient = this.client;
22101
- if (options.uploadEndpoint) this.uploadPresignClient = createS3Client({
22102
+ this.uploadPresignClient = options.uploadEndpoint ? createS3Client({
22102
22103
  ...options,
22103
22104
  endpoint: options.uploadEndpoint
22104
- });
22105
- else this.uploadPresignClient = this.client;
22105
+ }) : this.presignClient;
22106
22106
  }
22107
22107
  /** Build the full S3 object key from a cache key. */
22108
22108
  objectKey(key) {
@@ -27534,7 +27534,7 @@ init_manifest();
27534
27534
  const ALL_PLATFORMS = AgentPlatform.options;
27535
27535
  /** The orchestrator's own version (single-version invariant) = the packaged agent version. */
27536
27536
  function resolveKiciVersion() {
27537
- return "0.16.0";
27537
+ return "0.17.0";
27538
27538
  }
27539
27539
  /** Parse the --platform value: default set | single | CSV | `all`. */
27540
27540
  function parsePlatforms(raw) {
@@ -38830,7 +38830,7 @@ function routeRootArgs(root, args) {
38830
38830
  * where no bundler substituted the constant — the same shape `server.ts` and
38831
38831
  * `standalone.ts` use for `ORCHESTRATOR_VERSION`.
38832
38832
  */
38833
- const CLI_VERSION = "0.16.0";
38833
+ const CLI_VERSION = "0.17.0";
38834
38834
  /**
38835
38835
  * The kici-admin root command: a Commander `Command` whose own option parsing
38836
38836
  * first routes the argv through {@link routeRootArgs}. A leading `--version`
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ var S3CacheStorage = class {
43
43
  client;
44
44
  /** Separate client for pre-signed URL generation (uses externalEndpoint if configured). */
45
45
  presignClient;
46
- /** Separate client for the host CLI's pre-signed upload URL (uses uploadEndpoint if configured). */
46
+ /** Client for the developer upload URL: uploadEndpoint, else the presign client. */
47
47
  uploadPresignClient;
48
48
  bucket;
49
49
  prefix;
@@ -63,11 +63,10 @@ var S3CacheStorage = class {
63
63
  endpoint: options.externalEndpoint
64
64
  });
65
65
  else this.presignClient = this.client;
66
- if (options.uploadEndpoint) this.uploadPresignClient = createS3Client({
66
+ this.uploadPresignClient = options.uploadEndpoint ? createS3Client({
67
67
  ...options,
68
68
  endpoint: options.uploadEndpoint
69
- });
70
- else this.uploadPresignClient = this.client;
69
+ }) : this.presignClient;
71
70
  }
72
71
  /** Build the full S3 object key from a cache key. */
73
72
  objectKey(key) {
@@ -2978,9 +2977,17 @@ var AdminApiClient = class {
2978
2977
  async stopScalerOrphans(body) {
2979
2978
  return this.request("POST", "/api/v1/admin/scaler/orphans/stop", body);
2980
2979
  }
2980
+ /**
2981
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
2982
+ * (`kici-admin token create`). Removed in v1.0.0.
2983
+ */
2981
2984
  async createApiKey(opts) {
2982
2985
  return this.request("POST", "/api/v1/api-keys", opts);
2983
2986
  }
2987
+ /**
2988
+ * @deprecated No KiCI server serves `/api/v1/api-keys`; use `createToken`
2989
+ * with `routingKey` (`kici-admin token create --routing-key`). Removed in v1.0.0.
2990
+ */
2984
2991
  async addRoutingKeyPermission(keyId, pattern) {
2985
2992
  return this.request("POST", `/api/v1/api-keys/${encodeURIComponent(keyId)}/routing-permissions`, { pattern });
2986
2993
  }
@@ -60,14 +60,15 @@ export interface TestTriggerInput {
60
60
  /** Request trace ID from the HTTP request. */
61
61
  requestId: string;
62
62
  /**
63
- * The principal that initiated this run, relayed by the Platform on
64
- * `test.relay.trigger` (where the wire schema has always required it).
63
+ * The principal that initiated this run: the one the Platform relays on
64
+ * `test.relay.trigger`, or the admin token's service account on the direct
65
+ * test-run routes.
65
66
  *
66
- * Required rather than optional: the relay handler is the only production
67
- * caller, so there is no path that legitimately lacks an actor, and an
68
- * optional field would let a future caller silently drop attribution — which
69
- * is exactly how `execution_runs.triggered_by` stayed NULL for every remote
70
- * test run while the column claimed to hold the initiator.
67
+ * Required rather than optional: the relay handler and the direct test-run
68
+ * routes are the production callers and both always pass an actor, and an
69
+ * optional field would let a future caller silently drop attribution, which
70
+ * leaves `execution_runs.triggered_by` NULL while the column claims to hold
71
+ * the initiator.
71
72
  */
72
73
  actor: ActorPrincipal;
73
74
  /** JSON-stringified lock file content for local repos with no remote. */
@@ -70,6 +70,11 @@ export declare enum UnroutableCause {
70
70
  */
71
71
  OnlyOtherJobImageAgents = "only-other-job-image-agents"
72
72
  }
73
+ /**
74
+ * The operator-facing reason a job is `unroutable`, naming the exact selectors
75
+ * that went unmatched. Regex matchers are rendered as their source so the
76
+ * message stays readable rather than printing `[object Object]`.
77
+ */
73
78
  export declare function unroutableMessage(job: JobRoutingFacts, cause?: UnroutableCause): string;
74
79
  /**
75
80
  * Split the two reasons a queued job never ran: nothing in the fleet matched
@@ -40,6 +40,8 @@ export interface AuthTokenInfo {
40
40
  role: Role;
41
41
  routingKey: string | null;
42
42
  label: string;
43
+ /** The intended holder recorded at `token create --subject`, when one was. */
44
+ subject?: string | null;
43
45
  }
44
46
  /** The slice of TokenManager this middleware needs (keeps tests trivial to fake). */
45
47
  export interface AuthTokenValidator {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Caller identity for orchestrator HTTP routes that act on behalf of the
3
+ * person or tool presenting a credential.
4
+ *
5
+ * Today the one credential is an admin token, resolved through the shared
6
+ * bearer-auth boundary (`resolveBearerAuth`), so the 401 / 503 semantics are
7
+ * identical to every admin route. The caller is a tagged union so a later
8
+ * credential kind (a signed command, for one) joins as another variant without
9
+ * touching the routes that consume it.
10
+ */
11
+ import type { Context } from 'hono';
12
+ import { type ActorPrincipal } from '@kici-dev/engine';
13
+ import { type BearerAuthDeps } from './admin-auth.js';
14
+ import type { Role } from '../secrets/rbac.js';
15
+ /** A caller authenticated by an admin token. */
16
+ export interface TokenCaller {
17
+ kind: 'token';
18
+ tokenId: string;
19
+ label: string;
20
+ /** The intended holder recorded at `token create --subject`, or null. */
21
+ subject: string | null;
22
+ role: Role;
23
+ /** The single routing key a scoped token is restricted to, or null. */
24
+ routingKeyScope: string | null;
25
+ }
26
+ export type CallerOutcome = {
27
+ ok: true;
28
+ caller: TokenCaller;
29
+ } | {
30
+ ok: false;
31
+ status: 401 | 403 | 503;
32
+ error: string;
33
+ };
34
+ /**
35
+ * Resolve the request's caller.
36
+ *
37
+ * @param c - The request context (the `Authorization` header is read).
38
+ * @param deps - The token validator and log scope, as for `resolveBearerAuth`.
39
+ * @param opts.requireUnscoped - Refuse a routing-key-scoped token with 403.
40
+ * Routes whose routing key the server chooses set it: a scoped token has no
41
+ * key of its own to compare there, so accepting it would widen its reach.
42
+ * @returns The caller, or the status and body to answer with.
43
+ */
44
+ export declare function resolveCaller(c: Context<any>, deps: BearerAuthDeps, opts: {
45
+ requireUnscoped: boolean;
46
+ }): Promise<CallerOutcome>;
47
+ /**
48
+ * The principal a caller's actions are attributed to: a `service_account`
49
+ * bearing the admin token id, the same shape the admin held-run routes use.
50
+ */
51
+ export declare function callerActor(caller: TokenCaller): ActorPrincipal;
52
+ //# sourceMappingURL=caller-identity.d.ts.map
@@ -0,0 +1,46 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { OrchestratorMode } from '@kici-dev/engine';
3
+ import type { Database } from '../db/types.js';
4
+ import type { RbacEnforcer } from '../secrets/rbac.js';
5
+ import { type TestRelayHandlerDeps } from '../ws/test-relay-handlers.js';
6
+ import type { AuthTokenValidator } from './admin-auth.js';
7
+ import { type TokenCaller } from './caller-identity.js';
8
+ type TestDirectEnv = {
9
+ Variables: {
10
+ caller: TokenCaller;
11
+ };
12
+ };
13
+ /** Answer for a Platform-connected orchestrator that has no org yet. */
14
+ export declare const NO_PLATFORM_ORG_MESSAGE = "This orchestrator has not received its organization from the KiCI Platform yet, so it cannot choose where to run the test. Retry when the orchestrator is connected.";
15
+ export interface TestDirectRouteDeps {
16
+ tokenManager: AuthTokenValidator;
17
+ rbac: RbacEnforcer;
18
+ mode: OrchestratorMode;
19
+ /** The org a connected orchestrator learned from the Platform; read per request. */
20
+ platformOrgId: () => string | null | undefined;
21
+ db: Kysely<Database>;
22
+ /** Builds the relay handlers' deps for one request (minus org, routing key and source). */
23
+ relayDeps: () => Omit<TestRelayHandlerDeps, 'orgId' | 'routingKey' | 'accessLogSource'>;
24
+ }
25
+ /**
26
+ * The org a direct test run belongs to: the default org on an independent
27
+ * orchestrator, the Platform-assigned org otherwise (null until it is known).
28
+ */
29
+ export declare function directOrgId(mode: OrchestratorMode, platformOrgId: string | null | undefined): string | null;
30
+ /**
31
+ * Build the route deps, or null when admin auth is not configured: without a
32
+ * token store there is no credential to authenticate a developer with.
33
+ */
34
+ export declare function testDirectRouteDeps(input: {
35
+ adminDeps?: {
36
+ tokenManager: AuthTokenValidator;
37
+ rbac: RbacEnforcer;
38
+ };
39
+ }, rest: Omit<TestDirectRouteDeps, 'tokenManager' | 'rbac'>): TestDirectRouteDeps | null;
40
+ /**
41
+ * Build the direct test-run router: the routes under `/api/v1/test`, each
42
+ * behind admin-token authentication that refuses a routing-key-scoped token.
43
+ */
44
+ export declare function createTestDirectRoutes(deps: TestDirectRouteDeps): import("hono").Hono<TestDirectEnv, import("hono/types").BlankSchema, "/">;
45
+ export {};
46
+ //# sourceMappingURL=test-direct.d.ts.map
@@ -4,9 +4,9 @@
4
4
  * Mints an upload record and a presigned PUT URL + ephemeral X25519 public key
5
5
  * for the overlay tarball. The developer encrypts the tarball with the returned
6
6
  * public key and PUTs it directly to the object store; a test trigger then
7
- * references the upload id. This is invoked by the Platform-first
8
- * `test.relay.uploads.init` relay handler — the developer never reaches the
9
- * orchestrator's HTTP API directly.
7
+ * references the upload id. It is invoked by the `test.relay.uploads.init`
8
+ * relay handler (a run routed through the Platform) and by the direct
9
+ * test-run routes (`POST /api/v1/test/uploads/init`).
10
10
  */
11
11
  import type { Kysely } from 'kysely';
12
12
  import type { Database } from '../db/types.js';
@@ -19,12 +19,13 @@ export interface InitTestUploadParams {
19
19
  compressedSize?: number;
20
20
  /** PAT/actor identity that owns this upload, written to `test_uploads.created_by`. */
21
21
  createdBy?: string | null;
22
- /**
23
- * When true, presign with the host-facing internal endpoint. When false (the
24
- * Platform-relayed path), presign with the external/dev-reachable endpoint so
25
- * a developer on a different network can PUT directly to the object store.
26
- */
27
- internal?: boolean;
22
+ }
23
+ /**
24
+ * The orchestrator has no object storage, so it cannot mint an upload URL.
25
+ * Callers answer it as "service unavailable" with the message as-is.
26
+ */
27
+ export declare class TestUploadStorageUnavailableError extends Error {
28
+ constructor(message: string);
28
29
  }
29
30
  /** Result of {@link initTestUpload}. */
30
31
  export interface InitTestUploadResult {
@@ -41,8 +42,8 @@ export interface InitTestUploadDeps {
41
42
  /**
42
43
  * Mint an upload record and return a presigned PUT URL + ephemeral X25519
43
44
  * public key. The encryption keypair is generated per upload; the private key
44
- * is stored orchestrator-side for post-PUT decryption. The `internal` flag
45
- * selects the host-facing vs the external/dev-reachable presign endpoint.
45
+ * is stored orchestrator-side for post-PUT decryption. The URL is the
46
+ * developer-facing one (`CacheStorage.getInternalUploadUrl`).
46
47
  */
47
48
  export declare function initTestUpload(deps: InitTestUploadDeps, params: InitTestUploadParams): Promise<InitTestUploadResult>;
48
49
  //# sourceMappingURL=uploads.d.ts.map
@@ -23,7 +23,11 @@ export type Permission = 'context.create' | 'context.read' | 'context.update' |
23
23
  /** Stop live Firecracker VMs the node's orchestrator does not track. */
24
24
  | 'scaler.manage'
25
25
  /** Drop a departed peer from the coordinators' live peer registries. */
26
- | 'peer.manage';
26
+ | 'peer.manage'
27
+ /** Start or cancel a `kici run remote` test run through the direct test-run routes. */
28
+ | 'test_run.trigger'
29
+ /** Follow a test run's status and logs through the direct test-run routes. */
30
+ | 'test_run.read';
27
31
  /**
28
32
  * Error thrown when a role lacks the required permission.
29
33
  */
@@ -25,6 +25,7 @@
25
25
  * they use `requireUnscopedToken` instead.
26
26
  */
27
27
  import type { Context } from 'hono';
28
+ export declare const UNSCOPED_REQUIRED_MESSAGE = "Access denied: this route requires an unscoped admin token (the calling token is restricted to a single routing key)";
28
29
  /**
29
30
  * Reject the request with 403 when the calling token has a routing-key
30
31
  * scope and the request targets a different routing key.
@@ -40,6 +40,7 @@ export declare class TokenManager {
40
40
  role: Role;
41
41
  routingKey: string | null;
42
42
  label: string;
43
+ subject: string | null;
43
44
  } | null>;
44
45
  /**
45
46
  * Revoke a token by ID.