@alfe.ai/gateway 0.9.10 → 0.9.11

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 (2) hide show
  1. package/dist/health.js +301 -43
  2. package/package.json +2 -2
package/dist/health.js CHANGED
@@ -312,7 +312,7 @@ var AlfeApiClient = class {
312
312
  * would never reach the network — the `getToken: () => null` path would
313
313
  * synthesize a 401 and fire onAuthFailure, breaking the entire flow.
314
314
  */
315
- async _fetch(path, options, skipAuth = false) {
315
+ async _fetchResponse(path, options, skipAuth = false) {
316
316
  try {
317
317
  const url = `${this.apiBaseUrl}${path}`;
318
318
  const headers = new Headers(options?.headers);
@@ -348,14 +348,14 @@ var AlfeApiClient = class {
348
348
  }
349
349
  };
350
350
  }
351
- const text = res.status === 204 ? "" : await res.text();
352
- let body;
353
- if (text) try {
354
- body = JSON.parse(text);
355
- } catch {
356
- body = text;
357
- }
358
351
  if (!res.ok) {
352
+ const text = res.status === 204 ? "" : await res.text();
353
+ let body;
354
+ if (text) try {
355
+ body = JSON.parse(text);
356
+ } catch {
357
+ body = text;
358
+ }
359
359
  const errBody = body;
360
360
  return {
361
361
  ok: false,
@@ -368,8 +368,7 @@ var AlfeApiClient = class {
368
368
  }
369
369
  return {
370
370
  ok: true,
371
- res,
372
- body
371
+ res
373
372
  };
374
373
  } catch (err) {
375
374
  return {
@@ -381,6 +380,22 @@ var AlfeApiClient = class {
381
380
  };
382
381
  }
383
382
  }
383
+ async _fetch(path, options, skipAuth = false) {
384
+ const response = await this._fetchResponse(path, options, skipAuth);
385
+ if (!response.ok) return response;
386
+ const text = response.res.status === 204 ? "" : await response.res.text();
387
+ let body;
388
+ if (text) try {
389
+ body = JSON.parse(text);
390
+ } catch {
391
+ body = text;
392
+ }
393
+ return {
394
+ ok: true,
395
+ res: response.res,
396
+ body
397
+ };
398
+ }
384
399
  /**
385
400
  * Make an authenticated request to an Alfe API endpoint.
386
401
  * Unwraps the @auriclabs/api-core `{ data, timestamp, requestId }` envelope.
@@ -427,6 +442,30 @@ var AlfeApiClient = class {
427
442
  data: result.body
428
443
  };
429
444
  }
445
+ /**
446
+ * Make an authenticated request without attempting text/JSON decoding.
447
+ * Use for downloads such as a Teams app ZIP where UTF-8 conversion would
448
+ * corrupt the payload.
449
+ */
450
+ async binaryRequest(path, options) {
451
+ const result = await this._fetchResponse(path, options);
452
+ if (!result.ok) return result.result;
453
+ try {
454
+ return {
455
+ ok: true,
456
+ data: {
457
+ body: await result.res.arrayBuffer(),
458
+ contentType: result.res.headers.get("Content-Type") ?? void 0,
459
+ contentDisposition: result.res.headers.get("Content-Disposition") ?? void 0
460
+ }
461
+ };
462
+ } catch (err) {
463
+ return {
464
+ ok: false,
465
+ error: err instanceof Error ? err.message : "Failed to read binary response"
466
+ };
467
+ }
468
+ }
430
469
  getApiBaseUrl() {
431
470
  return this.apiBaseUrl;
432
471
  }
@@ -4227,6 +4266,176 @@ object({
4227
4266
  ttsModel: _enum(TTS_MODELS).optional(),
4228
4267
  enabled: boolean().optional()
4229
4268
  });
4269
+ //#endregion
4270
+ //#region ../../packages-internal/types/dist/access.js
4271
+ /**
4272
+ * Runtime catalog of declared permission subjects + allowed actions.
4273
+ *
4274
+ * Mirrors the compile-time `PermissionDefinitions` augmentation in
4275
+ * `@alfe/api-core/auriclabs-roles.ts`. Kept here so browser code
4276
+ * (dashboard) can consume it without pulling in server-only deps
4277
+ * (`@middy/core`, `aws-lambda`, etc.).
4278
+ */
4279
+ const PERMISSION_CATALOG = {
4280
+ all: ["manage"],
4281
+ user: [
4282
+ "create",
4283
+ "read",
4284
+ "update",
4285
+ "delete",
4286
+ "manage"
4287
+ ],
4288
+ agent: [
4289
+ "create",
4290
+ "read",
4291
+ "update",
4292
+ "delete",
4293
+ "exec",
4294
+ "chat",
4295
+ "manage"
4296
+ ],
4297
+ chat: [
4298
+ "read",
4299
+ "write",
4300
+ "upload",
4301
+ "manage"
4302
+ ],
4303
+ sync: [
4304
+ "read",
4305
+ "write",
4306
+ "manage"
4307
+ ],
4308
+ integration: [
4309
+ "create",
4310
+ "read",
4311
+ "update",
4312
+ "delete",
4313
+ "manage"
4314
+ ],
4315
+ gateway: ["connect", "manage"],
4316
+ voice: ["read", "manage"],
4317
+ identity: ["read", "manage"],
4318
+ token: [
4319
+ "create",
4320
+ "read",
4321
+ "update",
4322
+ "delete",
4323
+ "manage"
4324
+ ],
4325
+ billing: ["read", "manage"],
4326
+ template: [
4327
+ "create",
4328
+ "read",
4329
+ "update",
4330
+ "delete",
4331
+ "manage"
4332
+ ],
4333
+ team: [
4334
+ "create",
4335
+ "read",
4336
+ "update",
4337
+ "delete",
4338
+ "manage"
4339
+ ],
4340
+ project: [
4341
+ "create",
4342
+ "read",
4343
+ "update",
4344
+ "delete",
4345
+ "manage"
4346
+ ],
4347
+ org_file: ["read", "write"],
4348
+ org_knowledge: ["read", "write"],
4349
+ model_policy: ["read", "manage"],
4350
+ secret: [
4351
+ "create",
4352
+ "read",
4353
+ "update",
4354
+ "delete",
4355
+ "manage"
4356
+ ],
4357
+ database: [
4358
+ "read",
4359
+ "write",
4360
+ "manage"
4361
+ ],
4362
+ role_management: ["read", "manage"],
4363
+ org: ["read", "manage"],
4364
+ connection: [
4365
+ "create",
4366
+ "read",
4367
+ "update",
4368
+ "delete",
4369
+ "manage"
4370
+ ],
4371
+ remote: ["control", "terminal"],
4372
+ subscription: ["read", "manage"],
4373
+ referral: ["manage"],
4374
+ promo_code: ["manage"],
4375
+ pricing: ["manage"],
4376
+ access_gate: ["manage"],
4377
+ dlq: ["read", "manage"],
4378
+ support: ["read"],
4379
+ discounts: ["manage"],
4380
+ stats: ["read"],
4381
+ mobile: ["read", "manage"],
4382
+ impersonation: ["manage"]
4383
+ };
4384
+ /**
4385
+ * Subjects a tenant may grant via a custom role.
4386
+ *
4387
+ * Excludes `all` (platform-admin escape hatch) and every platform-only
4388
+ * permission subject listed after it in the catalog.
4389
+ */
4390
+ const TENANT_ALLOWED_SUBJECTS = [
4391
+ "agent",
4392
+ "chat",
4393
+ "sync",
4394
+ "integration",
4395
+ "secret",
4396
+ "database",
4397
+ "template",
4398
+ "org_file",
4399
+ "org_knowledge",
4400
+ "team",
4401
+ "project",
4402
+ "model_policy",
4403
+ "role_management",
4404
+ "billing",
4405
+ "identity",
4406
+ "token",
4407
+ "user",
4408
+ "gateway",
4409
+ "voice",
4410
+ "org",
4411
+ "connection",
4412
+ "remote",
4413
+ "subscription"
4414
+ ];
4415
+ /**
4416
+ * Subjects a PLATFORM ADMIN may grant via a custom **admin** role (authored at
4417
+ * the flat `admin:` scope, stored under the `"system"` tenant). Two groups:
4418
+ *
4419
+ * 1. Platform-only subjects — every catalog subject that is NOT in
4420
+ * `TENANT_ALLOWED_SUBJECTS` (and never `all`). Derived from
4421
+ * `PERMISSION_CATALOG` so a newly-declared admin-only subject becomes
4422
+ * grantable in custom admin roles automatically.
4423
+ * 2. Dual-surface subjects an admin also curates platform-wide: `user`
4424
+ * (platform user management), `role_management` (admin role authoring),
4425
+ * and `subscription` (reviewing every tenant's plan/startup application).
4426
+ *
4427
+ * `all` is deliberately excluded — it is reserved for the built-in
4428
+ * `sys:admin:admin` super-admin role and must never be grantable through a
4429
+ * custom admin role. The complement of `TENANT_ALLOWED_SUBJECTS` keeps the
4430
+ * tenant vocabulary and the admin vocabulary from drifting into each other.
4431
+ */
4432
+ const ADMIN_DUAL_SURFACE_SUBJECTS = [
4433
+ "user",
4434
+ "role_management",
4435
+ "subscription"
4436
+ ];
4437
+ const TENANT_ALLOWED_SUBJECT_LOOKUP = new Set(TENANT_ALLOWED_SUBJECTS);
4438
+ [...Object.keys(PERMISSION_CATALOG).filter((subject) => subject !== "all" && !TENANT_ALLOWED_SUBJECT_LOOKUP.has(subject)), ...ADMIN_DUAL_SURFACE_SUBJECTS];
4230
4439
  enumValues({
4231
4440
  Org: "org",
4232
4441
  Team: "team",
@@ -4592,7 +4801,7 @@ var AuthService = class {
4592
4801
  }
4593
4802
  prefix = "/auth";
4594
4803
  validate(token) {
4595
- return this.client.request(`${this.prefix}/validate`, {
4804
+ return this.client.publicRequest(`${this.prefix}/validate`, {
4596
4805
  method: "POST",
4597
4806
  body: JSON.stringify({ token })
4598
4807
  });
@@ -4967,6 +5176,12 @@ var IntegrationsService = class {
4967
5176
  listSlackChannels(agentId) {
4968
5177
  return this.client.request(`/slack/agents/${encodeURIComponent(agentId)}/channels`);
4969
5178
  }
5179
+ createSlackApp(agentId) {
5180
+ return this.client.request("/slack/apps/create", {
5181
+ method: "POST",
5182
+ body: JSON.stringify({ agentId })
5183
+ });
5184
+ }
4970
5185
  sendSlackMessage(agentId, channel, text) {
4971
5186
  return this.client.request(`/slack/agents/${encodeURIComponent(agentId)}/send`, {
4972
5187
  method: "POST",
@@ -6416,25 +6631,44 @@ var ConfigReconciler = class {
6416
6631
  status: "applied"
6417
6632
  };
6418
6633
  const failures = [];
6634
+ const inconclusiveKeys = [];
6419
6635
  for (const [key, want] of entries) try {
6420
6636
  if (getConfigRaw) {
6421
- if (await getConfigRaw(key) === want) {
6637
+ let current;
6638
+ try {
6639
+ current = await getConfigRaw(key);
6640
+ } catch (err) {
6641
+ log$4.warn({
6642
+ key,
6643
+ err: err instanceof Error ? err.message : String(err)
6644
+ }, "Config diff read failed — attempting the write");
6645
+ }
6646
+ if (current === want) {
6422
6647
  log$4.debug({ key }, "Config already at desired value — no-op");
6423
6648
  continue;
6424
6649
  }
6425
6650
  }
6426
6651
  await setConfigRaw(key, want);
6427
6652
  if (getConfigRaw) {
6428
- if (!await this.verify(getConfigRaw, key, want)) failures.push(`${key}: verify failed (read-back did not match)`);
6653
+ const outcome = await this.verify(getConfigRaw, key, want);
6654
+ if (outcome === "mismatched") failures.push(`${key}: verify failed (read-back did not match)`);
6655
+ else if (outcome === "inconclusive") inconclusiveKeys.push(key);
6429
6656
  }
6430
6657
  } catch (err) {
6431
6658
  const msg = err instanceof Error ? err.message : String(err);
6432
- if (getConfigRaw && await this.verify(getConfigRaw, key, want)) {
6433
- log$4.warn({
6434
- key,
6435
- err: msg
6436
- }, "config set errored but value landed — treating as applied");
6437
- continue;
6659
+ if (getConfigRaw) {
6660
+ const outcome = await this.verify(getConfigRaw, key, want);
6661
+ if (outcome === "matched") {
6662
+ log$4.warn({
6663
+ key,
6664
+ err: msg
6665
+ }, "config set errored but value landed — treating as applied");
6666
+ continue;
6667
+ }
6668
+ if (outcome === "inconclusive") {
6669
+ inconclusiveKeys.push(key);
6670
+ continue;
6671
+ }
6438
6672
  }
6439
6673
  failures.push(`${key}: ${msg}`);
6440
6674
  }
@@ -6451,6 +6685,13 @@ var ConfigReconciler = class {
6451
6685
  reason
6452
6686
  };
6453
6687
  }
6688
+ if (inconclusiveKeys.length > 0) {
6689
+ log$4.warn({
6690
+ version: desired.version,
6691
+ keys: inconclusiveKeys
6692
+ }, "Config reconcile verification was inconclusive — leaving version pending");
6693
+ return null;
6694
+ }
6454
6695
  log$4.info({
6455
6696
  version: desired.version,
6456
6697
  keys: entries.length
@@ -6461,11 +6702,21 @@ var ConfigReconciler = class {
6461
6702
  };
6462
6703
  }
6463
6704
  async verify(getConfigRaw, key, want) {
6705
+ let observedValue = false;
6464
6706
  for (let attempt = 0; attempt <= this.verifyRetries; attempt++) {
6465
- if (await getConfigRaw(key) === want) return true;
6707
+ try {
6708
+ const current = await getConfigRaw(key);
6709
+ if (current === want) return "matched";
6710
+ if (current !== void 0) observedValue = true;
6711
+ } catch (err) {
6712
+ log$4.debug({
6713
+ key,
6714
+ err: err instanceof Error ? err.message : String(err)
6715
+ }, "Config verify read failed");
6716
+ }
6466
6717
  if (attempt < this.verifyRetries && this.verifyRetryDelayMs > 0) await delay(this.verifyRetryDelayMs * 2 ** attempt);
6467
6718
  }
6468
- return false;
6719
+ return observedValue ? "mismatched" : "inconclusive";
6469
6720
  }
6470
6721
  };
6471
6722
  //#endregion
@@ -7735,7 +7986,7 @@ function warnFailedMcpServers(bundler, log, reason) {
7735
7986
  * List every server entry in the alfe bundler store. Lets agents inspect
7736
7987
  * what they've already registered before adding a new one.
7737
7988
  */
7738
- function handleMcpListServers(manager) {
7989
+ function handleMcpListServers(manager, liveStatusAvailable = manager?.serverStatuses !== void 0) {
7739
7990
  if (!manager) return {
7740
7991
  ok: false,
7741
7992
  error: {
@@ -7743,21 +7994,24 @@ function handleMcpListServers(manager) {
7743
7994
  message: "MCP manager not initialized"
7744
7995
  }
7745
7996
  };
7746
- const statuses = manager.serverStatuses ? manager.serverStatuses() : [];
7997
+ const statuses = liveStatusAvailable && manager.serverStatuses ? manager.serverStatuses() : [];
7747
7998
  const byName = new Map(statuses.map((s) => [s.name, s]));
7748
7999
  return {
7749
8000
  ok: true,
7750
- payload: { servers: manager.listServers().map(({ id, entry }) => ({
7751
- id,
7752
- entry: toPublicServerEntry(entry),
7753
- fingerprint: serverLaunchFingerprint(entry),
7754
- status: byName.get(id) ?? {
7755
- name: id,
7756
- connected: false,
7757
- toolCount: 0,
7758
- consecutiveFailures: 0
7759
- }
7760
- })) }
8001
+ payload: { servers: manager.listServers().map(({ id, entry }) => {
8002
+ const status = byName.get(id);
8003
+ return {
8004
+ id,
8005
+ entry: toPublicServerEntry(entry),
8006
+ fingerprint: serverLaunchFingerprint(entry),
8007
+ ...liveStatusAvailable ? { status: status ?? {
8008
+ name: id,
8009
+ connected: false,
8010
+ toolCount: 0,
8011
+ consecutiveFailures: 0
8012
+ } } : {}
8013
+ };
8014
+ }) }
7761
8015
  };
7762
8016
  }
7763
8017
  function toPublicServerEntry(entry) {
@@ -7786,10 +8040,12 @@ const MCP_ADD_WARM_TIMEOUT_MS = 12e3;
7786
8040
  * server actually works. Owned as `'manual'` so the agent can later remove it
7787
8041
  * without an expectedOwner conflict — matches what `alfe mcp add` does from the
7788
8042
  * CLI. Registration is durable regardless of the probe outcome: a probe
7789
- * failure (or a runtime with no daemon bundler, e.g. hermes) still returns
7790
- * `ok` with `connected: false`; the store watcher / runtime picks the entry up.
8043
+ * failure still returns `ok`; the store watcher / runtime picks the entry up.
8044
+ * A runtime that mirrors the store but owns its MCP children (Hermes and
8045
+ * Claude Code) has no daemon-side status authority, so it omits connected/tool
8046
+ * fields instead of presenting unknown as a definite disconnection.
7791
8047
  */
7792
- async function handleMcpAddServer(params, manager) {
8048
+ async function handleMcpAddServer(params, manager, liveStatusAvailable = manager?.warmServer !== void 0) {
7793
8049
  if (!manager) return {
7794
8050
  ok: false,
7795
8051
  error: {
@@ -7833,7 +8089,7 @@ async function handleMcpAddServer(params, manager) {
7833
8089
  };
7834
8090
  }
7835
8091
  let status = null;
7836
- if (manager.warmServer) try {
8092
+ if (liveStatusAvailable && manager.warmServer) try {
7837
8093
  status = await manager.warmServer(p.id, MCP_ADD_WARM_TIMEOUT_MS);
7838
8094
  } catch (err) {
7839
8095
  logger$1.warn({
@@ -7845,9 +8101,11 @@ async function handleMcpAddServer(params, manager) {
7845
8101
  ok: true,
7846
8102
  payload: {
7847
8103
  id: p.id,
7848
- connected: status?.connected ?? false,
7849
- toolCount: status?.toolCount ?? 0,
7850
- ...status?.lastError !== void 0 ? { error: status.lastError } : {}
8104
+ ...status ? {
8105
+ connected: status.connected,
8106
+ toolCount: status.toolCount,
8107
+ ...status.lastError !== void 0 ? { error: status.lastError } : {}
8108
+ } : {}
7851
8109
  }
7852
8110
  };
7853
8111
  }
@@ -25076,8 +25334,8 @@ function handlePluginRequest(method, params, pluginId) {
25076
25334
  case "integration.report": return Promise.resolve(handleIntegrationReport(params, pluginId));
25077
25335
  case "mcp.list_tools": return Promise.resolve(handleMcpListTools(mcpBundler));
25078
25336
  case "mcp.call_tool": return handleMcpCallTool(mcpBundler, params);
25079
- case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef));
25080
- case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef);
25337
+ case "mcp.list_servers": return Promise.resolve(handleMcpListServers(mcpManagerRef, mcpBundler !== null));
25338
+ case "mcp.add_server": return handleMcpAddServer(params, mcpManagerRef, mcpBundler !== null);
25081
25339
  case "mcp.remove_server": return handleMcpRemoveServer(params, mcpManagerRef);
25082
25340
  default: return Promise.resolve({
25083
25341
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/gateway",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "pino-roll": "^1.2.0",
24
24
  "smol-toml": ">=1.6.1",
25
25
  "ws": "^8.18.0",
26
- "@alfe.ai/agent-api-client": "^0.16.0",
26
+ "@alfe.ai/agent-api-client": "^0.16.1",
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.16",
28
28
  "@alfe.ai/config": "^0.4.1",
29
29
  "@alfe.ai/integration-manifest": "^0.3.6",