@akagilnc/pi-workflow-roles 0.1.3733 → 0.1.3741

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.
@@ -139,50 +139,6 @@ function parseAutoResumeLimit(value) {
139
139
  function setAutoResumeLimit(config, limit) {
140
140
  return { ...config, autoResumeLimit: parseAutoResumeLimit(limit) };
141
141
  }
142
- function requireNonEmptyToken(value, label) {
143
- const trimmed = value.trim();
144
- if (trimmed === "") {
145
- throw new Error(`${label} must be a non-empty string`);
146
- }
147
- return trimmed;
148
- }
149
- function setProviderHostAlias(config, provider, host, alias) {
150
- const from = requireNonEmptyToken(provider, "provider");
151
- const hostName = requireNonEmptyToken(host, "host");
152
- const to = requireNonEmptyToken(alias, "alias");
153
- const previous = config.providerAliases ?? {};
154
- return {
155
- ...config,
156
- providerAliases: {
157
- ...previous,
158
- [from]: {
159
- ...previous[from] ?? {},
160
- [hostName]: to
161
- }
162
- }
163
- };
164
- }
165
- function unsetProviderHostAlias(config, provider, host) {
166
- const from = requireNonEmptyToken(provider, "provider");
167
- const hostName = requireNonEmptyToken(host, "host");
168
- const previous = config.providerAliases;
169
- if (previous === void 0 || previous[from] === void 0) return config;
170
- const { [hostName]: _dropped, ...restHosts } = previous[from];
171
- const nextForProvider = Object.keys(restHosts).length === 0 ? void 0 : restHosts;
172
- const { [from]: _provider, ...restProviders } = previous;
173
- const nextAliases = nextForProvider === void 0 ? restProviders : { ...restProviders, [from]: nextForProvider };
174
- if (Object.keys(nextAliases).length === 0) {
175
- const { providerAliases: _gone, ...rest } = config;
176
- return rest;
177
- }
178
- return { ...config, providerAliases: nextAliases };
179
- }
180
- function applyProviderHostAlias(selection, host, aliases) {
181
- if (selection === void 0 || aliases === void 0) return selection;
182
- const mapped = aliases[selection.provider]?.[host];
183
- if (mapped === void 0) return selection;
184
- return { ...selection, provider: mapped };
185
- }
186
142
  function validatePublicCliConfigAxes(config, _packageRoot) {
187
143
  for (const seat of Object.keys(config.seats)) {
188
144
  const row = config.seats[seat];
@@ -252,44 +208,9 @@ function serializePublicCliConfig(config) {
252
208
  ...config.unknownSeats ?? {},
253
209
  ...config.seats
254
210
  },
255
- ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit },
256
- ...config.providerAliases === void 0 || Object.keys(config.providerAliases).length === 0 ? {} : { providerAliases: config.providerAliases }
211
+ ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit }
257
212
  };
258
213
  }
259
- function parseProviderHostAliases(value) {
260
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
261
- throw new Error("public CLI config.providerAliases must be an object");
262
- }
263
- const out = {};
264
- for (const [provider, byHost] of Object.entries(value)) {
265
- if (provider.trim() === "") {
266
- throw new Error("public CLI config.providerAliases provider key must be non-empty");
267
- }
268
- if (byHost === null || typeof byHost !== "object" || Array.isArray(byHost)) {
269
- throw new Error(
270
- `public CLI config.providerAliases[${provider}] must be an object`
271
- );
272
- }
273
- const hosts = {};
274
- for (const [host, alias] of Object.entries(byHost)) {
275
- if (host.trim() === "") {
276
- throw new Error(
277
- `public CLI config.providerAliases[${provider}] host key must be non-empty`
278
- );
279
- }
280
- if (typeof alias !== "string" || alias.trim() === "") {
281
- throw new Error(
282
- `public CLI config.providerAliases[${provider}][${host}] must be a non-empty string`
283
- );
284
- }
285
- hosts[host] = alias;
286
- }
287
- if (Object.keys(hosts).length > 0) {
288
- out[provider] = hosts;
289
- }
290
- }
291
- return out;
292
- }
293
214
  function parsePublicCliConfig(value) {
294
215
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
295
216
  throw new Error("public CLI config must be an object");
@@ -299,13 +220,6 @@ function parsePublicCliConfig(value) {
299
220
  if (record.autoResumeLimit !== void 0) {
300
221
  autoResumeLimit = parseAutoResumeLimit(record.autoResumeLimit);
301
222
  }
302
- let providerAliases;
303
- if (record.providerAliases !== void 0) {
304
- const parsed = parseProviderHostAliases(record.providerAliases);
305
- if (Object.keys(parsed).length > 0) {
306
- providerAliases = parsed;
307
- }
308
- }
309
223
  const unknownSeats = {};
310
224
  if (record.unknownSeats !== void 0) {
311
225
  if (record.unknownSeats === null || typeof record.unknownSeats !== "object" || Array.isArray(record.unknownSeats)) {
@@ -316,7 +230,6 @@ function parsePublicCliConfig(value) {
316
230
  const withOpaque = (seats2) => ({
317
231
  seats: seats2,
318
232
  ...autoResumeLimit === void 0 ? {} : { autoResumeLimit },
319
- ...providerAliases === void 0 ? {} : { providerAliases },
320
233
  ...Object.keys(unknownSeats).length === 0 ? {} : { unknownSeats }
321
234
  });
322
235
  if (record.seats === void 0) {
@@ -493,21 +406,11 @@ function resolveEffectiveSeat(config, seat, credentials, invocation) {
493
406
  };
494
407
  }
495
408
  }
496
- const withAxes = attachHostAxis(
409
+ return attachHostAxis(
497
410
  attachEngineAxis(modelSeat, config, invocation),
498
411
  config,
499
412
  invocation
500
413
  );
501
- const selection = applyProviderHostAlias(
502
- withAxes.selection,
503
- withAxes.host,
504
- config.providerAliases
505
- );
506
- if (selection === void 0) {
507
- const { selection: _dropped, ...rest } = withAxes;
508
- return rest;
509
- }
510
- return { ...withAxes, selection };
511
414
  }
512
415
  function effectiveSeatConfigurations(config, credentials, invocation) {
513
416
  return PUBLIC_CONFIGURABLE_SEATS.map(
@@ -540,7 +443,6 @@ async function loadCredentialProviders(agentDir) {
540
443
  }
541
444
  export {
542
445
  GATE_OFFICER_SEATS,
543
- applyProviderHostAlias,
544
446
  buildSeatModelCliArgs,
545
447
  clearPersistentSeatConfig,
546
448
  credentialProvidersFromAuthData,
@@ -561,7 +463,5 @@ export {
561
463
  setPersistentSeatConfig,
562
464
  setPersistentSeatEngine,
563
465
  setPersistentSeatHost,
564
- setProviderHostAlias,
565
- unsetProviderHostAlias,
566
466
  validatePublicCliConfigAxes
567
467
  };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * #788: owner-editable host provider map + host-directory resolution.
3
+ *
4
+ * Data file `~/.ak-roles/host-providers.json` is the sole owner-written map.
5
+ * Shape: { "<host>": { "<seat-provider>": "<host-provider>" } }.
6
+ * Code only reads it — no built-in pairs, no registration commands.
7
+ *
8
+ * Priority: table > unique directory match > loud failure.
9
+ * This ticket implements directory query for hermes only.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ export function hostProvidersPath(home) {
14
+ if (typeof home !== "string" || home.trim() === "") {
15
+ throw new Error("home must be explicitly provided");
16
+ }
17
+ return join(home, ".ak-roles", "host-providers.json");
18
+ }
19
+ /** Hermes provider model catalog under the operator home. */
20
+ export function hermesProviderModelsCachePath(home) {
21
+ return join(home, ".hermes", "provider_models_cache.json");
22
+ }
23
+ export function parseHostProvidersTable(value) {
24
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
25
+ throw new Error("host-providers.json must be an object");
26
+ }
27
+ const out = {};
28
+ for (const [host, byProvider] of Object.entries(value)) {
29
+ if (host.trim() === "") {
30
+ throw new Error("host-providers.json host key must be non-empty");
31
+ }
32
+ if (byProvider === null ||
33
+ typeof byProvider !== "object" ||
34
+ Array.isArray(byProvider)) {
35
+ throw new Error(`host-providers.json[${host}] must be an object`);
36
+ }
37
+ const providers = {};
38
+ for (const [seatProvider, hostProvider] of Object.entries(byProvider)) {
39
+ if (seatProvider.trim() === "") {
40
+ throw new Error(`host-providers.json[${host}] seat-provider key must be non-empty`);
41
+ }
42
+ if (typeof hostProvider !== "string" || hostProvider.trim() === "") {
43
+ throw new Error(`host-providers.json[${host}][${seatProvider}] must be a non-empty string`);
44
+ }
45
+ providers[seatProvider] = hostProvider;
46
+ }
47
+ if (Object.keys(providers).length > 0) {
48
+ out[host] = providers;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ /**
54
+ * Load the owner map. Missing file → empty table (directory / pass-through next).
55
+ * Malformed file fails loud — owner edits this by hand.
56
+ */
57
+ export function loadHostProvidersTable(home) {
58
+ const path = hostProvidersPath(home);
59
+ try {
60
+ const raw = readFileSync(path, "utf8");
61
+ return parseHostProvidersTable(JSON.parse(raw));
62
+ }
63
+ catch (error) {
64
+ if (error instanceof Error &&
65
+ "code" in error &&
66
+ error.code === "ENOENT") {
67
+ return {};
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ /** True when the host catalog entry names the seat model (bare or slash-suffixed). */
73
+ export function hostCatalogOffersModel(catalogModels, seatModel) {
74
+ return catalogModels.some((entry) => entry === seatModel || entry.endsWith(`/${seatModel}`));
75
+ }
76
+ /**
77
+ * Providers in the hermes catalog that offer `seatModel`.
78
+ * Reads `~/.hermes/provider_models_cache.json` under the given home.
79
+ * Missing file (ENOENT) → empty list (zero → loud failure upstream).
80
+ * Other read/parse failures keep their identity — never washed as "no model".
81
+ */
82
+ export function listHermesProvidersForModel(home, seatModel) {
83
+ const path = hermesProviderModelsCachePath(home);
84
+ let text;
85
+ try {
86
+ text = readFileSync(path, "utf8");
87
+ }
88
+ catch (error) {
89
+ if (error instanceof Error &&
90
+ "code" in error &&
91
+ error.code === "ENOENT") {
92
+ return [];
93
+ }
94
+ throw error;
95
+ }
96
+ const raw = JSON.parse(text);
97
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
98
+ throw new Error(`hermes provider_models_cache.json must be an object: ${path}`);
99
+ }
100
+ const found = [];
101
+ for (const [provider, entry] of Object.entries(raw)) {
102
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
103
+ continue;
104
+ }
105
+ const models = entry.models;
106
+ if (!Array.isArray(models))
107
+ continue;
108
+ const names = models.filter((m) => typeof m === "string");
109
+ if (hostCatalogOffersModel(names, seatModel)) {
110
+ found.push(provider);
111
+ }
112
+ }
113
+ return found.sort();
114
+ }
115
+ /** Hosts this build knows how to query a provider-model directory for. */
116
+ function listDirectoryProvidersForModel(home, host, seatModel) {
117
+ if (host === "hermes") {
118
+ return listHermesProvidersForModel(home, seatModel);
119
+ }
120
+ // No directory for this host → caller falls through to pass-through.
121
+ return undefined;
122
+ }
123
+ export class HostProviderResolutionError extends Error {
124
+ host;
125
+ seatProvider;
126
+ seatModel;
127
+ candidates;
128
+ constructor(options) {
129
+ super(options.message);
130
+ this.name = "HostProviderResolutionError";
131
+ this.host = options.host;
132
+ this.seatProvider = options.seatProvider;
133
+ this.seatModel = options.seatModel;
134
+ this.candidates = options.candidates ?? [];
135
+ }
136
+ }
137
+ /**
138
+ * Project seat-table provider to the host-facing name.
139
+ * Priority: owner table > unique host-directory match > loud failure.
140
+ * Hosts without a directory and without a table entry pass the seat provider through.
141
+ */
142
+ export function projectHostFacingProvider(selection, host, table, home) {
143
+ if (selection === undefined)
144
+ return undefined;
145
+ const mapped = table[host]?.[selection.provider];
146
+ if (mapped !== undefined) {
147
+ return mapped === selection.provider
148
+ ? selection
149
+ : { ...selection, provider: mapped };
150
+ }
151
+ const directory = listDirectoryProvidersForModel(home, host, selection.model);
152
+ if (directory === undefined) {
153
+ // No directory for this host → pass-through (pi, grok-build, …).
154
+ return selection;
155
+ }
156
+ if (directory.length === 1) {
157
+ const only = directory[0];
158
+ return only === selection.provider
159
+ ? selection
160
+ : { ...selection, provider: only };
161
+ }
162
+ if (directory.length === 0) {
163
+ throw new HostProviderResolutionError({
164
+ message: `host ${host} has no provider offering model ${selection.model}; seat provider was ${selection.provider}`,
165
+ host,
166
+ seatProvider: selection.provider,
167
+ seatModel: selection.model,
168
+ });
169
+ }
170
+ throw new HostProviderResolutionError({
171
+ message: `host ${host} has multiple providers for model ${selection.model}: ${directory.join(", ")}; set host-providers.json[${host}][${selection.provider}] to one of them`,
172
+ host,
173
+ seatProvider: selection.provider,
174
+ seatModel: selection.model,
175
+ candidates: directory,
176
+ });
177
+ }
178
+ /** Render the owner table for `config show` (disk face, unchanged). */
179
+ export function renderHostProvidersTable(table) {
180
+ const lines = [];
181
+ for (const host of Object.keys(table).sort()) {
182
+ const byProvider = table[host];
183
+ for (const seatProvider of Object.keys(byProvider).sort()) {
184
+ lines.push(`hostProvider\t${host}\t${seatProvider}\t${byProvider[seatProvider]}`);
185
+ }
186
+ }
187
+ return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
188
+ }