@akagilnc/pi-workflow-roles 0.1.3737 → 0.1.3744

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.
@@ -1068,7 +1068,7 @@ const SUPPORT_COMMAND_HELP = {
1068
1068
  },
1069
1069
  config: {
1070
1070
  command: "config",
1071
- summary: "Persistent seat model, labor-engine, host, and auto-resume defaults.",
1071
+ summary: "Persistent seat model, labor-engine, host, and auto-resume defaults. Host providers live in ~/.ak-roles/host-providers.json (owner-edited).",
1072
1072
  usage: [
1073
1073
  "ak-role config set <seat> <provider/model[:thinking]> [<seat> <spec> ...]",
1074
1074
  "ak-role config unset <gatekeeper|inspector|notary>",
@@ -1077,8 +1077,6 @@ const SUPPORT_COMMAND_HELP = {
1077
1077
  "ak-role config set-host <seat> <name>",
1078
1078
  "ak-role config unset-host <seat>",
1079
1079
  "ak-role config set-auto-resume-limit <N>",
1080
- "ak-role config set-provider-alias <provider> <host> <alias>",
1081
- "ak-role config unset-provider-alias <provider> <host>",
1082
1080
  ],
1083
1081
  examples: [
1084
1082
  "ak-role config set judge openai-codex/gpt-5.6-sol:high",
@@ -1086,7 +1084,6 @@ const SUPPORT_COMMAND_HELP = {
1086
1084
  "ak-role config set-engine judge opus",
1087
1085
  "ak-role config set-host judge grok-build",
1088
1086
  "ak-role config set-auto-resume-limit 3",
1089
- "ak-role config set-provider-alias xai hermes xai-oauth",
1090
1087
  ],
1091
1088
  },
1092
1089
  help: {
@@ -70,9 +70,6 @@ function projectSeatEngine(seat) {
70
70
  function projectSeatHost(seat) {
71
71
  return seat.host === void 0 ? {} : { host: seat.host };
72
72
  }
73
- function projectSeatModel(seat) {
74
- return seat.selection === void 0 ? {} : { model: seat.selection };
75
- }
76
73
  async function createSummonEnv(options) {
77
74
  const [{ piDurablePrincipalAuthority }, { appendPiSessionCustomEntry, createPiRoleTurnHost }] = await Promise.all([
78
75
  import("./pi/durable-principal.js"),
@@ -122,6 +119,13 @@ async function createSummonEnv(options) {
122
119
  }
123
120
  };
124
121
  }
122
+ const { loadHostProvidersTable, projectHostFacingProvider } = await import("./public-cli/host-providers.js");
123
+ const hostFacingSelection = options.seat.selection === void 0 ? void 0 : projectHostFacingProvider(
124
+ options.seat.selection,
125
+ hostName,
126
+ loadHostProvidersTable(options.home),
127
+ options.home
128
+ );
125
129
  return {
126
130
  home: options.home,
127
131
  principalAuthority,
@@ -131,7 +135,7 @@ async function createSummonEnv(options) {
131
135
  roleTurnHost,
132
136
  cwd: options.cwd,
133
137
  credentials: options.credentials,
134
- ...projectSeatModel(options.seat),
138
+ ...hostFacingSelection === void 0 ? {} : { model: hostFacingSelection },
135
139
  ...projectSeatEngine(options.seat),
136
140
  ...projectSeatHost(options.seat)
137
141
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3737",
3
+ "version": "0.1.3744",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -28,14 +28,17 @@ import {
28
28
  setPersistentSeatConfig,
29
29
  setPersistentSeatEngine,
30
30
  setPersistentSeatHost,
31
- setProviderHostAlias,
32
- unsetProviderHostAlias,
33
31
  validatePublicCliConfigAxes,
34
32
  type CredentialProviders,
35
33
  type EffectiveSeat,
36
34
  type InvocationModelOverride,
37
35
  type PublicCliConfig,
38
36
  } from "./config.ts";
37
+ import {
38
+ loadHostProvidersTable,
39
+ projectHostFacingProvider,
40
+ renderHostProvidersTable,
41
+ } from "./host-providers.ts";
39
42
  import { seatModelOnly } from "./registry.ts";
40
43
  import { CliUsageError } from "./cli-errors.ts";
41
44
  import type { CliIo } from "./cli-io.ts";
@@ -428,23 +431,36 @@ function createRoleEnvironment(
428
431
 
429
432
  // #617 DK-3: resume and new legs share one seat resolution — model/host/engine
430
433
  // come from the live seat table (invocation flag → persistent → default).
434
+ // #788 expectation 2: select a registered host first; only then project the
435
+ // host-facing provider (table > unique directory > fail). Bad host never
436
+ // reaches model resolution.
437
+ const roleTurnHost = resolveRoleTurnHost(env, {
438
+ role,
439
+ seat: options.seat,
440
+ principalAuthority: env.principalAuthority!,
441
+ ...(extraPiArgs === undefined ? {} : { extraPiArgs }),
442
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
443
+ });
444
+ const hostFacingSelection =
445
+ options.seat.selection === undefined
446
+ ? undefined
447
+ : projectHostFacingProvider(
448
+ options.seat.selection,
449
+ options.seat.host,
450
+ loadHostProvidersTable(options.home),
451
+ options.home,
452
+ );
431
453
  return {
432
454
  home: options.home,
433
455
  principalAuthority: env.principalAuthority!,
434
456
  agentDir: options.agentDir,
435
457
  sessionAppender: appendPiSessionCustomEntry,
436
458
  packageRoot: env.packageRoot,
437
- roleTurnHost: resolveRoleTurnHost(env, {
438
- role,
439
- seat: options.seat,
440
- principalAuthority: env.principalAuthority!,
441
- ...(extraPiArgs === undefined ? {} : { extraPiArgs }),
442
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
443
- }),
459
+ roleTurnHost,
444
460
  cwd: options.cwd,
445
461
  ...(options.credentials === undefined ? {} : { credentials: options.credentials }),
446
462
  ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
447
- ...(options.seat.selection === undefined ? {} : { model: options.seat.selection }),
463
+ ...(hostFacingSelection === undefined ? {} : { model: hostFacingSelection }),
448
464
  ...projectSeatEngine(options.seat),
449
465
  ...projectSeatHost(options.seat),
450
466
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
@@ -771,7 +787,7 @@ function renderHelp(): string {
771
787
  "Persistent config: ak-role config set <seat> <provider/model[:thinking]> | unset <gatekeeper|inspector|notary>",
772
788
  "Persistent engine (callable roles): ak-role config set-engine <seat> <name> | unset-engine <seat>",
773
789
  "Persistent host (callable roles): ak-role config set-host <seat> <name> | unset-host <seat>",
774
- "Provider host alias: ak-role config set-provider-alias <provider> <host> <alias> | unset-provider-alias <provider> <host>",
790
+ "Host providers: ~/.ak-roles/host-providers.json (owner-edited; table > unique host directory > fail)",
775
791
  "Host resolution: --host → persistent seat host → pi (resume uses the same order; #617)",
776
792
  "Effective seats: ak-role roles",
777
793
  );
@@ -900,7 +916,7 @@ function renderConfigDisplaySeat(row: ConfigDisplaySeat): string {
900
916
  return `${row.seat}\t${row.source}\t${model}\t${engine}\t${host}`;
901
917
  }
902
918
 
903
- function renderConfig(config: PublicCliConfig): string {
919
+ function renderConfig(config: PublicCliConfig, home: string): string {
904
920
  const lines: string[] = ["seat\tsource\tmodel\tengine\thost"];
905
921
  const rows = projectConfigDisplaySeats(config);
906
922
  if (rows.length === 0) {
@@ -912,17 +928,9 @@ function renderConfig(config: PublicCliConfig): string {
912
928
  }
913
929
  // #422: show the effective auto-resume ceiling (configured value or default).
914
930
  lines.push(`autoResumeLimit\t${config.autoResumeLimit ?? AUTO_RESUME_LIMIT}`);
915
- // #778: owner-registered provider→host aliases (disk face; seats stay as written).
916
- const aliases = config.providerAliases;
917
- if (aliases !== undefined) {
918
- for (const provider of Object.keys(aliases).sort()) {
919
- const byHost = aliases[provider]!;
920
- for (const host of Object.keys(byHost).sort()) {
921
- lines.push(`providerAlias\t${provider}\t${host}\t${byHost[host]}`);
922
- }
923
- }
924
- }
925
- return `${lines.join("\n")}\n`;
931
+ // #788: owner host-providers table as written on disk (separate file).
932
+ const hostProvidersBlock = renderHostProvidersTable(loadHostProvidersTable(home));
933
+ return `${lines.join("\n")}\n${hostProvidersBlock}`;
926
934
  }
927
935
 
928
936
  async function runConfigCommand(
@@ -942,7 +950,7 @@ async function runConfigCommand(
942
950
  );
943
951
  return 0;
944
952
  }
945
- io.stdout(renderConfig(config));
953
+ io.stdout(renderConfig(config, home));
946
954
  return 0;
947
955
  }
948
956
 
@@ -971,7 +979,7 @@ async function runConfigCommand(
971
979
  config = setPersistentSeatConfig(config, seat, parseModelSpec(spec));
972
980
  }
973
981
  await savePublicCliConfig(config, home);
974
- io.stdout(renderConfig(config));
982
+ io.stdout(renderConfig(config, home));
975
983
  return 0;
976
984
  }
977
985
 
@@ -993,7 +1001,7 @@ async function runConfigCommand(
993
1001
  seat,
994
1002
  );
995
1003
  await savePublicCliConfig(config, home);
996
- io.stdout(renderConfig(config));
1004
+ io.stdout(renderConfig(config, home));
997
1005
  return 0;
998
1006
  }
999
1007
 
@@ -1011,7 +1019,7 @@ async function runConfigCommand(
1011
1019
  throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
1012
1020
  }
1013
1021
  await savePublicCliConfig(config, home);
1014
- io.stdout(renderConfig(config));
1022
+ io.stdout(renderConfig(config, home));
1015
1023
  return 0;
1016
1024
  }
1017
1025
 
@@ -1035,7 +1043,7 @@ async function runConfigCommand(
1035
1043
  );
1036
1044
  }
1037
1045
  await savePublicCliConfig(config, home);
1038
- io.stdout(renderConfig(config));
1046
+ io.stdout(renderConfig(config, home));
1039
1047
  return 0;
1040
1048
  }
1041
1049
 
@@ -1057,7 +1065,7 @@ async function runConfigCommand(
1057
1065
  );
1058
1066
  }
1059
1067
  await savePublicCliConfig(config, home);
1060
- io.stdout(renderConfig(config));
1068
+ io.stdout(renderConfig(config, home));
1061
1069
  return 0;
1062
1070
  }
1063
1071
 
@@ -1090,48 +1098,7 @@ async function runConfigCommand(
1090
1098
  let config = await loadAndValidateConfig(home, packageRoot);
1091
1099
  config = setAutoResumeLimit(config, converted);
1092
1100
  await savePublicCliConfig(config, home);
1093
- io.stdout(renderConfig(config));
1094
- return 0;
1095
- }
1096
-
1097
- // #778: owner-registered provider→host alias (opaque strings; no host catalog).
1098
- if (args[0] === "set-provider-alias") {
1099
- if (args.length !== 4) {
1100
- throw new CliUsageError(
1101
- "usage: ak-role config set-provider-alias <provider> <host> <alias>",
1102
- );
1103
- }
1104
- let config = await loadAndValidateConfig(home, packageRoot);
1105
- try {
1106
- config = setProviderHostAlias(config, args[1]!, args[2]!, args[3]!);
1107
- } catch (error) {
1108
- throw new CliUsageError(
1109
- error instanceof Error ? error.message : String(error),
1110
- { cause: error },
1111
- );
1112
- }
1113
- await savePublicCliConfig(config, home);
1114
- io.stdout(renderConfig(config));
1115
- return 0;
1116
- }
1117
-
1118
- if (args[0] === "unset-provider-alias") {
1119
- if (args.length !== 3) {
1120
- throw new CliUsageError(
1121
- "usage: ak-role config unset-provider-alias <provider> <host>",
1122
- );
1123
- }
1124
- let config = await loadAndValidateConfig(home, packageRoot);
1125
- try {
1126
- config = unsetProviderHostAlias(config, args[1]!, args[2]!);
1127
- } catch (error) {
1128
- throw new CliUsageError(
1129
- error instanceof Error ? error.message : String(error),
1130
- { cause: error },
1131
- );
1132
- }
1133
- await savePublicCliConfig(config, home);
1134
- io.stdout(renderConfig(config));
1101
+ io.stdout(renderConfig(config, home));
1135
1102
  return 0;
1136
1103
  }
1137
1104
 
@@ -54,15 +54,6 @@ export type PersistentSeatConfig = {
54
54
  host?: string;
55
55
  };
56
56
 
57
- /**
58
- * #778: owner-registered provider name by host. Keyed seat-table provider →
59
- * host → host-facing provider. Package code carries the map only — no built-in
60
- * pairs. Unregistered (provider, host) pairs pass the seat provider through.
61
- */
62
- export type ProviderHostAliases = Readonly<
63
- Record<string, Readonly<Record<string, string>>>
64
- >;
65
-
66
57
  export type PublicCliConfig = {
67
58
  seats: Partial<Record<PublicConfigurableSeat, PersistentSeatConfig>>;
68
59
  /**
@@ -72,11 +63,6 @@ export type PublicCliConfig = {
72
63
  * bound (ADR 0035).
73
64
  */
74
65
  autoResumeLimit?: number;
75
- /**
76
- * #778: owner-registered provider→host aliases. Sibling of `seats`; never
77
- * invents pairs. Absent or empty means pure pass-through.
78
- */
79
- providerAliases?: ProviderHostAliases;
80
66
  /**
81
67
  * #592: opaque seat rows this build does not own. Carried through every
82
68
  * parse→save cycle so a write never silently erases neighboring-line rows
@@ -307,81 +293,6 @@ export function setAutoResumeLimit(
307
293
  return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
308
294
  }
309
295
 
310
- function requireNonEmptyToken(value: string, label: string): string {
311
- const trimmed = value.trim();
312
- if (trimmed === "") {
313
- throw new Error(`${label} must be a non-empty string`);
314
- }
315
- return trimmed;
316
- }
317
-
318
- /**
319
- * #778: register one owner provider→host alias. Opaque strings only — no host
320
- * catalog check and no built-in provider pairs. Sibling keys (seats included)
321
- * are preserved.
322
- */
323
- export function setProviderHostAlias(
324
- config: PublicCliConfig,
325
- provider: string,
326
- host: string,
327
- alias: string,
328
- ): PublicCliConfig {
329
- const from = requireNonEmptyToken(provider, "provider");
330
- const hostName = requireNonEmptyToken(host, "host");
331
- const to = requireNonEmptyToken(alias, "alias");
332
- const previous = config.providerAliases ?? {};
333
- return {
334
- ...config,
335
- providerAliases: {
336
- ...previous,
337
- [from]: {
338
- ...(previous[from] ?? {}),
339
- [hostName]: to,
340
- },
341
- },
342
- };
343
- }
344
-
345
- /** #778: drop one owner provider→host alias. Empty map collapses to absent. */
346
- export function unsetProviderHostAlias(
347
- config: PublicCliConfig,
348
- provider: string,
349
- host: string,
350
- ): PublicCliConfig {
351
- const from = requireNonEmptyToken(provider, "provider");
352
- const hostName = requireNonEmptyToken(host, "host");
353
- const previous = config.providerAliases;
354
- if (previous === undefined || previous[from] === undefined) return config;
355
- const { [hostName]: _dropped, ...restHosts } = previous[from]!;
356
- const nextForProvider =
357
- Object.keys(restHosts).length === 0 ? undefined : restHosts;
358
- const { [from]: _provider, ...restProviders } = previous;
359
- const nextAliases =
360
- nextForProvider === undefined
361
- ? restProviders
362
- : { ...restProviders, [from]: nextForProvider };
363
- if (Object.keys(nextAliases).length === 0) {
364
- const { providerAliases: _gone, ...rest } = config;
365
- return rest;
366
- }
367
- return { ...config, providerAliases: nextAliases };
368
- }
369
-
370
- /**
371
- * #778: project seat-table provider through owner aliases for one host.
372
- * Unregistered pairs return the seat selection unchanged (pass-through).
373
- */
374
- export function applyProviderHostAlias(
375
- selection: SeatModelConfig | undefined,
376
- host: string,
377
- aliases: ProviderHostAliases | undefined,
378
- ): SeatModelConfig | undefined {
379
- if (selection === undefined || aliases === undefined) return selection;
380
- const mapped = aliases[selection.provider]?.[host];
381
- if (mapped === undefined) return selection;
382
- return { ...selection, provider: mapped };
383
- }
384
-
385
296
  /**
386
297
  * Config-parse seam: persistent call axes belong to PUBLIC_CALLABLE_ROLES;
387
298
  * engine names need only path-safety syntax (no closed material catalog;
@@ -476,7 +387,6 @@ export function buildSeatModelCliArgs(model: SeatModelConfig | undefined): strin
476
387
  function serializePublicCliConfig(config: PublicCliConfig): {
477
388
  seats: Record<string, unknown>;
478
389
  autoResumeLimit?: number;
479
- providerAliases?: ProviderHostAliases;
480
390
  } {
481
391
  return {
482
392
  // Known seats win on any key clash; by construction the two maps are
@@ -488,48 +398,9 @@ function serializePublicCliConfig(config: PublicCliConfig): {
488
398
  ...(config.autoResumeLimit === undefined
489
399
  ? {}
490
400
  : { autoResumeLimit: config.autoResumeLimit }),
491
- ...(config.providerAliases === undefined ||
492
- Object.keys(config.providerAliases).length === 0
493
- ? {}
494
- : { providerAliases: config.providerAliases }),
495
401
  };
496
402
  }
497
403
 
498
- function parseProviderHostAliases(value: unknown): ProviderHostAliases {
499
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
500
- throw new Error("public CLI config.providerAliases must be an object");
501
- }
502
- const out: Record<string, Record<string, string>> = {};
503
- for (const [provider, byHost] of Object.entries(value as Record<string, unknown>)) {
504
- if (provider.trim() === "") {
505
- throw new Error("public CLI config.providerAliases provider key must be non-empty");
506
- }
507
- if (byHost === null || typeof byHost !== "object" || Array.isArray(byHost)) {
508
- throw new Error(
509
- `public CLI config.providerAliases[${provider}] must be an object`,
510
- );
511
- }
512
- const hosts: Record<string, string> = {};
513
- for (const [host, alias] of Object.entries(byHost as Record<string, unknown>)) {
514
- if (host.trim() === "") {
515
- throw new Error(
516
- `public CLI config.providerAliases[${provider}] host key must be non-empty`,
517
- );
518
- }
519
- if (typeof alias !== "string" || alias.trim() === "") {
520
- throw new Error(
521
- `public CLI config.providerAliases[${provider}][${host}] must be a non-empty string`,
522
- );
523
- }
524
- hosts[host] = alias;
525
- }
526
- if (Object.keys(hosts).length > 0) {
527
- out[provider] = hosts;
528
- }
529
- }
530
- return out;
531
- }
532
-
533
404
  function parsePublicCliConfig(value: unknown): PublicCliConfig {
534
405
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
535
406
  throw new Error("public CLI config must be an object");
@@ -537,7 +408,6 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
537
408
  const record = value as {
538
409
  seats?: unknown;
539
410
  autoResumeLimit?: unknown;
540
- providerAliases?: unknown;
541
411
  unknownSeats?: unknown;
542
412
  };
543
413
  // #422 round-trip preservation: the sibling top-level key must survive every
@@ -546,15 +416,6 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
546
416
  if (record.autoResumeLimit !== undefined) {
547
417
  autoResumeLimit = parseAutoResumeLimit(record.autoResumeLimit);
548
418
  }
549
- // #778: same survival duty as autoResumeLimit — owner aliases must not vanish
550
- // on an unrelated seat write.
551
- let providerAliases: ProviderHostAliases | undefined;
552
- if (record.providerAliases !== undefined) {
553
- const parsed = parseProviderHostAliases(record.providerAliases);
554
- if (Object.keys(parsed).length > 0) {
555
- providerAliases = parsed;
556
- }
557
- }
558
419
  // #592: opaque bucket may already be present on an in-memory round-trip
559
420
  // (load→set→save calls parse again). Carry it before seats iteration so a
560
421
  // disk-shaped seats map and a memory-shaped bucket both survive.
@@ -572,7 +433,6 @@ function parsePublicCliConfig(value: unknown): PublicCliConfig {
572
433
  const withOpaque = (seats: PublicCliConfig["seats"]): PublicCliConfig => ({
573
434
  seats,
574
435
  ...(autoResumeLimit === undefined ? {} : { autoResumeLimit }),
575
- ...(providerAliases === undefined ? {} : { providerAliases }),
576
436
  ...(Object.keys(unknownSeats).length === 0 ? {} : { unknownSeats }),
577
437
  });
578
438
  if (record.seats === undefined) {
@@ -818,23 +678,13 @@ export function resolveEffectiveSeat(
818
678
  }
819
679
  }
820
680
 
821
- const withAxes = attachHostAxis(
681
+ // Seat axes only. Host-facing provider projection (#788) runs after the host
682
+ // is selected as registered — never before (bad host must not reach model).
683
+ return attachHostAxis(
822
684
  attachEngineAxis(modelSeat, config, invocation),
823
685
  config,
824
686
  invocation,
825
687
  );
826
- // #778: host-facing provider is the seat-table value after owner aliases.
827
- // Disk seats stay as written; only the effective selection is projected.
828
- const selection = applyProviderHostAlias(
829
- withAxes.selection,
830
- withAxes.host,
831
- config.providerAliases,
832
- );
833
- if (selection === undefined) {
834
- const { selection: _dropped, ...rest } = withAxes;
835
- return rest;
836
- }
837
- return { ...withAxes, selection };
838
688
  }
839
689
 
840
690
  export function effectiveSeatConfigurations(