mlclaw 0.3.1 → 0.3.4

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.
@@ -26,7 +26,7 @@ var DEFAULT_BRAND_NAME = "ML Claw";
26
26
  var DEFAULT_THEME_COLOR = "#111827";
27
27
  var DEFAULT_LOGO_ASSET = "mlclaw.svg";
28
28
  var DEFAULT_HUGGING_FACE_ASSET = "hf-logo.svg";
29
- var DEFAULT_ASSISTANT_AVATAR_ASSET = "assistant-avatar.svg";
29
+ var DEFAULT_HUGGING_FACE_PNG_ASSET = "hf-logo.png";
30
30
  function resolveBranding(env, agentName) {
31
31
  const defaultName = defaultBrandName(agentName);
32
32
  const name = cleanText(env.MLCLAW_BRAND_NAME) ?? defaultName;
@@ -41,16 +41,13 @@ function resolveBranding(env, agentName) {
41
41
  ),
42
42
  favicon32Asset: normalizeAssetRef(
43
43
  env.MLCLAW_BRAND_FAVICON_32 ?? env.MLCLAW_BRAND_FAVICON_PNG ?? env.MLCLAW_BRAND_FAVICON,
44
- DEFAULT_HUGGING_FACE_ASSET
44
+ DEFAULT_HUGGING_FACE_PNG_ASSET
45
45
  ),
46
46
  faviconIcoAsset: normalizeAssetRef(
47
47
  env.MLCLAW_BRAND_FAVICON_ICO ?? env.MLCLAW_BRAND_FAVICON,
48
48
  DEFAULT_HUGGING_FACE_ASSET
49
49
  ),
50
- appleTouchIconAsset: normalizeAssetRef(
51
- env.MLCLAW_BRAND_APPLE_TOUCH_ICON ?? env.MLCLAW_BRAND_ASSISTANT_AVATAR,
52
- DEFAULT_ASSISTANT_AVATAR_ASSET
53
- )
50
+ appleTouchIconAsset: normalizeAssetRef(env.MLCLAW_BRAND_APPLE_TOUCH_ICON, DEFAULT_HUGGING_FACE_PNG_ASSET)
54
51
  };
55
52
  }
56
53
  function publicBranding(branding) {
@@ -62,33 +59,37 @@ function publicBranding(branding) {
62
59
  };
63
60
  }
64
61
  function brandingManifest(branding) {
65
- return `${JSON.stringify({
66
- name: branding.name,
67
- short_name: branding.shortName,
68
- description: `${branding.name} browser gateway`,
69
- start_url: "./",
70
- display: "standalone",
71
- theme_color: branding.themeColor,
72
- background_color: branding.themeColor,
73
- icons: [
74
- {
75
- src: "./favicon.svg",
76
- sizes: "any",
77
- type: "image/svg+xml",
78
- purpose: "any"
79
- },
80
- {
81
- src: "./favicon-32.png",
82
- sizes: "32x32",
83
- type: "image/png"
84
- },
85
- {
86
- src: "./apple-touch-icon.png",
87
- sizes: "180x180",
88
- type: "image/png"
89
- }
90
- ]
91
- }, null, 2)}
62
+ return `${JSON.stringify(
63
+ {
64
+ name: branding.name,
65
+ short_name: branding.shortName,
66
+ description: `${branding.name} browser gateway`,
67
+ start_url: "./",
68
+ display: "standalone",
69
+ theme_color: branding.themeColor,
70
+ background_color: branding.themeColor,
71
+ icons: [
72
+ {
73
+ src: "./favicon.svg",
74
+ sizes: "any",
75
+ type: "image/svg+xml",
76
+ purpose: "any"
77
+ },
78
+ {
79
+ src: "./favicon-32.png",
80
+ sizes: "32x32",
81
+ type: "image/png"
82
+ },
83
+ {
84
+ src: "./apple-touch-icon.png",
85
+ sizes: "180x180",
86
+ type: "image/png"
87
+ }
88
+ ]
89
+ },
90
+ null,
91
+ 2
92
+ )}
92
93
  `;
93
94
  }
94
95
  function defaultBrandName(agentName) {
@@ -4697,14 +4698,13 @@ var approvalSchema = external_exports.object({
4697
4698
  pending_expires_at: external_exports.string().datetime({ offset: true }).optional(),
4698
4699
  active_expires_at: external_exports.string().datetime({ offset: true }).optional(),
4699
4700
  requested_duration_seconds: external_exports.number().int().positive().safe(),
4700
- requested_max_uses: external_exports.number().int().positive().safe(),
4701
+ requested_max_uses: external_exports.number().int().positive().safe().nullable(),
4701
4702
  granted_max_uses: external_exports.number().int().positive().safe().nullable(),
4702
4703
  used_count: external_exports.number().int().nonnegative().safe(),
4703
4704
  request_reason: external_exports.string().max(2e3).optional(),
4704
4705
  decided_at: external_exports.string().datetime({ offset: true }).optional(),
4705
4706
  decided_by: external_exports.string().max(200).optional(),
4706
4707
  decided_on_behalf_of: external_exports.string().max(200).optional(),
4707
- decision_reason: external_exports.string().max(2e3).optional(),
4708
4708
  presentation: external_exports.object({
4709
4709
  risk: external_exports.enum(["unknown", "low", "medium", "high", "critical"]),
4710
4710
  title: external_exports.string().min(1).max(200),
@@ -4712,10 +4712,10 @@ var approvalSchema = external_exports.object({
4712
4712
  facts: external_exports.array(displayFieldSchema).max(20).optional()
4713
4713
  }).strict(),
4714
4714
  presentation_unavailable: external_exports.boolean().optional(),
4715
- allowed_actions: external_exports.array(external_exports.enum(["approve", "deny", "cancel", "revoke"])).max(4),
4715
+ allowed_actions: external_exports.array(external_exports.enum(["approve", "deny", "revoke"])).max(3),
4716
4716
  approval_bounds: external_exports.object({
4717
4717
  max_duration_seconds: external_exports.number().int().positive().safe(),
4718
- max_uses: external_exports.number().int().positive().safe()
4718
+ max_uses: external_exports.number().int().positive().safe().nullable()
4719
4719
  }).strict().optional()
4720
4720
  }).strict();
4721
4721
  var approvalPageSchema = external_exports.object({
@@ -4808,11 +4808,10 @@ var BrokerOperatorClient = class {
4808
4808
  expected_revision: decision.expectedRevision,
4809
4809
  idempotency_key: decision.idempotencyKey,
4810
4810
  on_behalf_of: decision.onBehalfOf,
4811
- ...decision.reason ? { decision_reason: decision.reason } : {},
4812
- ...decision.durationSeconds || decision.maxUses ? {
4811
+ ...decision.durationSeconds !== void 0 || decision.maxUses !== void 0 ? {
4813
4812
  constraints: {
4814
- ...decision.durationSeconds ? { duration_seconds: decision.durationSeconds } : {},
4815
- ...decision.maxUses ? { max_uses: decision.maxUses } : {}
4813
+ ...decision.durationSeconds !== void 0 ? { duration_seconds: decision.durationSeconds } : {},
4814
+ ...decision.maxUses !== void 0 ? { max_uses: decision.maxUses } : {}
4816
4815
  }
4817
4816
  } : {}
4818
4817
  })
@@ -5105,7 +5104,9 @@ function loadConfig(env = process.env) {
5105
5104
  routerToken: trim(env.MLCLAW_ROUTER_TOKEN ?? env.HF_ROUTER_TOKEN),
5106
5105
  brokerAgentUrl: trim(env.MLCLAW_HF_BROKER_URL),
5107
5106
  brokerAgentSecret: readOptionalSecret(trim(env.MLCLAW_HF_BROKER_AGENT_SECRET_FILE)),
5107
+ brokerAgentSecretFile: trim(env.MLCLAW_HF_BROKER_AGENT_SECRET_FILE),
5108
5108
  operatorBrokers: loadOperatorBrokers(trim(env.MLCLAW_OPERATOR_BROKERS_FILE)),
5109
+ brokerKitPopoverDecisions: env.MLCLAW_BROKERKIT_POPOVER_DECISIONS === "1" || env.MLCLAW_BROKERKIT_POPOVER_DECISIONS === "true",
5109
5110
  hubUrl: trim(env.HF_ENDPOINT) ?? "https://huggingface.co",
5110
5111
  openaiCredentialFile: trim(env.MLCLAW_OPENAI_CREDENTIAL_FILE) ?? "/tmp/mlclaw-secrets/openai.env",
5111
5112
  openaiCredentialStoreFile,
@@ -7360,7 +7361,81 @@ function signatureMatches(a, b) {
7360
7361
  }
7361
7362
 
7362
7363
  // src/mlclaw-space-runtime/delegated-brokerkit.ts
7363
- import { createHash, createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7364
+ import { createHash, createHmac as createHmac2, randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7365
+
7366
+ // src/mlclaw-space-runtime/delegated-revisions.ts
7367
+ import { randomBytes as randomBytes3 } from "node:crypto";
7368
+ var DelegatedRevisions = class {
7369
+ epoch = randomBytes3(16).toString("base64url");
7370
+ revision = 0;
7371
+ material = "";
7372
+ current;
7373
+ waiters = /* @__PURE__ */ new Set();
7374
+ publish(material, value) {
7375
+ if (this.current && material === this.material) return this.current;
7376
+ this.material = material;
7377
+ this.revision += 1;
7378
+ this.current = value(this.cursor());
7379
+ for (const waiter of [...this.waiters])
7380
+ this.finish(waiter, { api_version: "brokerkit.io/operator-ui/v1", cursor: this.cursor(), changed: true });
7381
+ return this.current;
7382
+ }
7383
+ wait(cursor, waitSeconds, signal) {
7384
+ const observed = this.parse(cursor);
7385
+ if (observed === void 0) return Promise.reject(revisionError("cursor_expired"));
7386
+ if (observed !== this.revision) {
7387
+ return Promise.resolve({ api_version: "brokerkit.io/operator-ui/v1", cursor: this.cursor(), changed: true });
7388
+ }
7389
+ if (this.waiters.size >= 256) return Promise.reject(revisionError("source_unavailable"));
7390
+ if (signal?.aborted) return Promise.reject(abortError());
7391
+ return new Promise((resolve, reject) => {
7392
+ const waiter = {
7393
+ resolve,
7394
+ reject,
7395
+ timer: setTimeout(() => {
7396
+ this.finish(waiter, { api_version: "brokerkit.io/operator-ui/v1", cursor: this.cursor(), changed: false });
7397
+ }, waitSeconds * 1e3),
7398
+ ...signal ? { signal } : {}
7399
+ };
7400
+ waiter.timer.unref();
7401
+ if (signal) {
7402
+ waiter.abort = () => this.fail(waiter, abortError());
7403
+ signal.addEventListener("abort", waiter.abort, { once: true });
7404
+ }
7405
+ this.waiters.add(waiter);
7406
+ });
7407
+ }
7408
+ cursor() {
7409
+ return `${this.epoch}.${this.revision.toString(36)}`;
7410
+ }
7411
+ parse(value) {
7412
+ const match2 = /^([A-Za-z0-9_-]{22})\.([0-9a-z]{1,13})$/u.exec(value);
7413
+ if (!match2 || match2[1] !== this.epoch) return void 0;
7414
+ const revision = Number.parseInt(match2[2] ?? "", 36);
7415
+ return Number.isSafeInteger(revision) && revision <= this.revision ? revision : void 0;
7416
+ }
7417
+ finish(waiter, value) {
7418
+ this.cleanup(waiter);
7419
+ waiter.resolve(value);
7420
+ }
7421
+ fail(waiter, error) {
7422
+ this.cleanup(waiter);
7423
+ waiter.reject(error);
7424
+ }
7425
+ cleanup(waiter) {
7426
+ if (!this.waiters.delete(waiter)) return;
7427
+ clearTimeout(waiter.timer);
7428
+ if (waiter.signal && waiter.abort) waiter.signal.removeEventListener("abort", waiter.abort);
7429
+ }
7430
+ };
7431
+ function revisionError(code) {
7432
+ return Object.assign(new Error(code), { code });
7433
+ }
7434
+ function abortError() {
7435
+ return new DOMException("The operation was aborted", "AbortError");
7436
+ }
7437
+
7438
+ // src/mlclaw-space-runtime/delegated-brokerkit.ts
7364
7439
  var API_VERSION = "brokerkit.io/delegated-web/v1";
7365
7440
  var TOKEN_LIFETIME_SECONDS = 4 * 60;
7366
7441
  var MAX_PAGES_PER_SOURCE = 32;
@@ -7377,7 +7452,8 @@ var DelegatedBrokerKit = class {
7377
7452
  handles = /* @__PURE__ */ new Map();
7378
7453
  handlesByIdentity = /* @__PURE__ */ new Map();
7379
7454
  snapshotInFlight;
7380
- issueSession(actor) {
7455
+ revisions = new DelegatedRevisions();
7456
+ issueSession(actor, access) {
7381
7457
  const issuedAt = Math.floor(this.now().getTime() / 1e3);
7382
7458
  const expiresAt = issuedAt + TOKEN_LIFETIME_SECONDS;
7383
7459
  const payload = {
@@ -7386,25 +7462,27 @@ var DelegatedBrokerKit = class {
7386
7462
  subject: actor,
7387
7463
  issuedAt,
7388
7464
  expiresAt,
7389
- nonce: randomBytes3(16).toString("base64url")
7465
+ nonce: randomBytes4(16).toString("base64url"),
7466
+ access
7390
7467
  };
7391
7468
  const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
7392
7469
  const signature = this.sign(encoded);
7393
7470
  return {
7394
7471
  api_version: API_VERSION,
7395
- actor,
7396
- decision_token: `${encoded}.${signature}`,
7397
- expires_at: new Date(expiresAt * 1e3).toISOString()
7472
+ token: `${encoded}.${signature}`,
7473
+ expires_at: new Date(expiresAt * 1e3).toISOString(),
7474
+ access,
7475
+ renewal_transport: "direct"
7398
7476
  };
7399
7477
  }
7400
- authorize(header) {
7401
- return this.authorizeSession(header)?.actor;
7478
+ authorize(token) {
7479
+ return this.authorizeSession(token)?.actor;
7402
7480
  }
7403
- authorizeSession(header) {
7404
- const encoded = authenticatedTokenPayload(header, (value) => this.sign(value));
7481
+ authorizeSession(token) {
7482
+ const encoded = authenticatedTokenPayload(token, (value) => this.sign(value));
7405
7483
  if (!encoded) return void 0;
7406
7484
  const payload = parseTokenPayload(encoded);
7407
- return payload && tokenIsCurrent(payload, this.now()) ? { actor: payload.subject, sessionId: payload.nonce } : void 0;
7485
+ return payload && tokenIsCurrent(payload, this.now()) ? { actor: payload.subject, sessionId: payload.nonce, access: payload.access } : void 0;
7408
7486
  }
7409
7487
  async snapshot() {
7410
7488
  if (this.snapshotInFlight) return this.snapshotInFlight;
@@ -7424,12 +7502,50 @@ var DelegatedBrokerKit = class {
7424
7502
  );
7425
7503
  const selected = selectSnapshotRequests(results, MAX_HANDLES);
7426
7504
  const reservedHandles = this.selectedExistingHandles(selected);
7505
+ const sources = results.map((result) => result.source);
7506
+ const requests = selected.map(
7507
+ ({ source, request }) => project(source, request, this.handle(source.id, request, reservedHandles))
7508
+ );
7509
+ const material = JSON.stringify({
7510
+ sources: sources.map((source) => ({
7511
+ id: source.id,
7512
+ label: source.label,
7513
+ healthy: source.healthy,
7514
+ ...source.error ? { error: source.error } : {}
7515
+ })),
7516
+ requests
7517
+ });
7518
+ return this.revisions.publish(material, (cursor) => ({
7519
+ api_version: "brokerkit.io/operator-ui/v1",
7520
+ cursor,
7521
+ sources,
7522
+ requests,
7523
+ synchronized_at: synchronizedAt
7524
+ }));
7525
+ }
7526
+ async events(cursor, waitSeconds, signal) {
7527
+ await this.snapshot();
7528
+ const waiting = this.revisions.wait(cursor, waitSeconds, signal);
7529
+ const refresh = setInterval(() => void this.snapshot().catch(() => void 0), 1e3);
7530
+ refresh.unref();
7531
+ try {
7532
+ return await waiting;
7533
+ } catch (error) {
7534
+ if (error instanceof Error && "code" in error && (error.code === "cursor_expired" || error.code === "source_unavailable")) {
7535
+ throw delegatedError(error.code);
7536
+ }
7537
+ throw error;
7538
+ } finally {
7539
+ clearInterval(refresh);
7540
+ }
7541
+ }
7542
+ async summary() {
7543
+ const snapshot = await this.snapshot();
7427
7544
  return {
7428
- sources: results.map((result) => result.source),
7429
- requests: selected.map(
7430
- ({ source, request }) => project(source, request, this.handle(source.id, request, reservedHandles))
7431
- ),
7432
- synchronizedAt
7545
+ api_version: snapshot.api_version,
7546
+ cursor: snapshot.cursor,
7547
+ pending: snapshot.requests.filter((value) => value.request.status === "pending").length,
7548
+ healthy: snapshot.sources.every((source) => source.healthy)
7433
7549
  };
7434
7550
  }
7435
7551
  async detail(handle) {
@@ -7467,7 +7583,7 @@ var DelegatedBrokerKit = class {
7467
7583
  ]);
7468
7584
  const requests = reconcileRequests(pages.map((page2) => page2.requests));
7469
7585
  return {
7470
- source: deadline.signal.aborted ? { ...summary, healthy: false, error: "broker_timeout" } : pages.some((page2) => page2.truncated) ? { ...summary, healthy: false, error: "source_truncated" } : { ...summary, healthy: true, lastSyncAt: synchronizedAt },
7586
+ source: deadline.signal.aborted ? { ...summary, healthy: false, error: "broker_timeout" } : pages.some((page2) => page2.truncated) ? { ...summary, healthy: false, error: "source_truncated" } : { ...summary, healthy: true, last_sync_at: synchronizedAt },
7471
7587
  requests
7472
7588
  };
7473
7589
  } catch (error) {
@@ -7512,7 +7628,7 @@ var DelegatedBrokerKit = class {
7512
7628
  if (this.handles.size >= MAX_HANDLES && !this.pruneOldestHandle(reservedHandles)) {
7513
7629
  throw delegatedError("source_unavailable");
7514
7630
  }
7515
- const handle = randomBytes3(18).toString("base64url");
7631
+ const handle = randomBytes4(18).toString("base64url");
7516
7632
  const requestExpiry = Date.parse(handleExpiry(request));
7517
7633
  const expiresAtMs = Number.isFinite(requestExpiry) ? Math.min(requestExpiry, this.now().getTime() + 24 * 60 * 6e4) : this.now().getTime() + 5 * 6e4;
7518
7634
  this.handles.set(handle, { sourceId, requestId: request.id, revision: request.revision, expiresAtMs });
@@ -7611,7 +7727,7 @@ function delegatedError(code) {
7611
7727
  return new DelegatedBrokerKitError(code);
7612
7728
  }
7613
7729
  function project(source, request, handle) {
7614
- return { ...request, sourceId: source.id, sourceLabel: source.label, handle };
7730
+ return { source_id: source.id, source_label: source.label, handle, request };
7615
7731
  }
7616
7732
  function requestIdentity(sourceId, requestId, revision) {
7617
7733
  return `${sourceId}\0${requestId}\0${revision}`;
@@ -7633,18 +7749,22 @@ function decisionOptions(record, action, expectedRevision, actor, options) {
7633
7749
  expectedRevision,
7634
7750
  idempotencyKey: decisionKey(record, action, actor),
7635
7751
  onBehalfOf: `mlclaw:${actor}`,
7636
- ...options.reason ? { reason: options.reason } : {},
7637
7752
  ...options.durationSeconds ? { durationSeconds: options.durationSeconds } : {},
7638
- ...options.maxUses ? { maxUses: options.maxUses } : {}
7753
+ ...options.maxUses !== void 0 ? { maxUses: options.maxUses } : {}
7639
7754
  };
7640
7755
  }
7641
7756
  function decisionWithinBounds(action, request, options) {
7642
7757
  if (options.durationSeconds === void 0 && options.maxUses === void 0) return true;
7643
7758
  const bounds = request.approval_bounds;
7644
7759
  return Boolean(
7645
- action === "approve" && bounds && options.durationSeconds !== void 0 && options.durationSeconds <= bounds.max_duration_seconds && options.maxUses !== void 0 && options.maxUses <= bounds.max_uses
7760
+ action === "approve" && bounds && options.durationSeconds !== void 0 && options.durationSeconds <= bounds.max_duration_seconds && useLimitWithinBounds(options.maxUses, bounds.max_uses)
7646
7761
  );
7647
7762
  }
7763
+ function useLimitWithinBounds(requested, maximum) {
7764
+ if (requested === void 0) return false;
7765
+ if (requested === null) return maximum === null;
7766
+ return maximum === null || requested <= maximum;
7767
+ }
7648
7768
  function assertDecisionAllowed(request, record, action, expectedRevision, options) {
7649
7769
  if (request.revision !== record.revision || request.revision !== expectedRevision) {
7650
7770
  throw delegatedError("revision_stale");
@@ -7653,10 +7773,8 @@ function assertDecisionAllowed(request, record, action, expectedRevision, option
7653
7773
  throw delegatedError("action_not_allowed");
7654
7774
  }
7655
7775
  }
7656
- function authenticatedTokenPayload(header, sign3) {
7657
- if (!header?.startsWith("Bearer ")) return void 0;
7658
- const token = header.slice("Bearer ".length);
7659
- if (token.length > 4096) return void 0;
7776
+ function authenticatedTokenPayload(token, sign3) {
7777
+ if (!token || token.length > 4096 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u.test(token)) return void 0;
7660
7778
  const [encoded, signature, extra] = token.split(".");
7661
7779
  return encoded && signature && extra === void 0 && safeEqual(signature, sign3(encoded)) ? encoded : void 0;
7662
7780
  }
@@ -7678,10 +7796,10 @@ function validTokenPayload(value) {
7678
7796
  return hasExactTokenFields(record) && validTokenIdentity(record) && validTokenTimes(record) && validTokenNonce(record);
7679
7797
  }
7680
7798
  function hasExactTokenFields(record) {
7681
- return Object.keys(record).sort().join(",") === "audience,expiresAt,issuedAt,nonce,subject,version";
7799
+ return Object.keys(record).sort().join(",") === "access,audience,expiresAt,issuedAt,nonce,subject,version";
7682
7800
  }
7683
7801
  function validTokenIdentity(record) {
7684
- return record.version === 1 && record.audience === "brokerkit-delegated-web" && typeof record.subject === "string" && record.subject.length >= 1 && record.subject.length <= 200;
7802
+ return record.version === 1 && record.audience === "brokerkit-delegated-web" && (record.access === "read" || record.access === "decide") && typeof record.subject === "string" && record.subject.length >= 1 && record.subject.length <= 200;
7685
7803
  }
7686
7804
  function validTokenTimes(record) {
7687
7805
  return typeof record.issuedAt === "number" && Number.isSafeInteger(record.issuedAt) && typeof record.expiresAt === "number" && Number.isSafeInteger(record.expiresAt);
@@ -8405,6 +8523,8 @@ function remainingUpstreamTimeout(deadline) {
8405
8523
  }
8406
8524
 
8407
8525
  // src/mlclaw-space-runtime/openclaw-config.ts
8526
+ var BROKER_MCP_CONNECTION_TIMEOUT_MS = 1e4;
8527
+ var BROKER_MCP_REQUEST_TIMEOUT_MS = 45e3;
8408
8528
  async function configureOpenClawGateway(config2) {
8409
8529
  const raw2 = await fs.readFile(config2.openclawConfigPath, "utf8");
8410
8530
  const openclawConfig = JSON.parse(raw2);
@@ -8429,6 +8549,7 @@ async function configureOpenClawGateway(config2) {
8429
8549
  };
8430
8550
  configureOpenClawModels(openclawConfig, config2);
8431
8551
  configureManagedMcpServers(openclawConfig, config2);
8552
+ configureBrokerMcpServer(openclawConfig, config2);
8432
8553
  configureBrokerKitPlugin(openclawConfig, config2);
8433
8554
  await fs.mkdir(path.dirname(config2.openclawConfigPath), { recursive: true });
8434
8555
  await fs.writeFile(config2.openclawConfigPath, `${JSON.stringify(openclawConfig, null, 2)}
@@ -8438,6 +8559,60 @@ async function configureOpenClawGateway(config2) {
8438
8559
  await fs.chown(config2.openclawConfigPath, config2.openclawUid, config2.openclawGid);
8439
8560
  }
8440
8561
  }
8562
+ function configureBrokerMcpServer(openclawConfig, config2) {
8563
+ const servers = object(object(openclawConfig, "mcp"), "servers");
8564
+ if (!config2.brokerAgentUrl || !config2.brokerAgentSecretFile) {
8565
+ delete servers["huggingface-broker"];
8566
+ return;
8567
+ }
8568
+ const existing = objectValue2(servers["huggingface-broker"]);
8569
+ servers["huggingface-broker"] = {
8570
+ ...preservedBrokerMcpFields(existing),
8571
+ command: "/usr/local/bin/hf-broker",
8572
+ args: ["mcp"],
8573
+ connectionTimeoutMs: BROKER_MCP_CONNECTION_TIMEOUT_MS,
8574
+ requestTimeoutMs: BROKER_MCP_REQUEST_TIMEOUT_MS,
8575
+ env: {
8576
+ HF_BROKER_AGENT_ENDPOINT: brokerAgentEndpoint(config2.brokerAgentUrl),
8577
+ HF_BROKER_SHARED_SECRET_FILE: config2.brokerAgentSecretFile
8578
+ },
8579
+ ...existing?.enabled === false ? { enabled: false } : { enabled: true }
8580
+ };
8581
+ }
8582
+ function brokerAgentEndpoint(agentUrl) {
8583
+ const parsed = new URL(agentUrl);
8584
+ if (parsed.protocol !== "http:" || !parsed.port || parsed.username || parsed.password) {
8585
+ throw new Error("HF Broker agent URL must be an unauthenticated HTTP URL with an explicit port");
8586
+ }
8587
+ return `tcp://${parsed.host}`;
8588
+ }
8589
+ function preservedBrokerMcpFields(existing) {
8590
+ const codex = preservedBrokerCodexConfig(objectValue2(existing?.codex));
8591
+ return {
8592
+ ...existing?.toolFilter && typeof existing.toolFilter === "object" ? { toolFilter: existing.toolFilter } : {},
8593
+ ...typeof existing?.supportsParallelToolCalls === "boolean" ? { supportsParallelToolCalls: existing.supportsParallelToolCalls } : {},
8594
+ ...codex ? { codex } : {}
8595
+ };
8596
+ }
8597
+ function preservedBrokerCodexConfig(existing) {
8598
+ const agents = brokerAgentScope(existing?.agents);
8599
+ const defaultToolsApprovalMode = brokerApprovalMode(existing?.defaultToolsApprovalMode);
8600
+ const nativeApprovalMode = brokerApprovalMode(existing?.default_tools_approval_mode);
8601
+ const preserved = {
8602
+ ...agents ? { agents } : {},
8603
+ ...defaultToolsApprovalMode ? { defaultToolsApprovalMode } : {},
8604
+ ...nativeApprovalMode ? { default_tools_approval_mode: nativeApprovalMode } : {}
8605
+ };
8606
+ return Object.keys(preserved).length > 0 ? preserved : void 0;
8607
+ }
8608
+ function brokerAgentScope(value) {
8609
+ if (!Array.isArray(value)) return void 0;
8610
+ const agents = value.filter((agent) => typeof agent === "string" && /^[a-z0-9][a-z0-9_-]{0,63}$/iu.test(agent.trim())).map((agent) => agent.trim());
8611
+ return agents.length > 0 ? agents : void 0;
8612
+ }
8613
+ function brokerApprovalMode(value) {
8614
+ return value === "auto" || value === "prompt" || value === "approve" ? value : void 0;
8615
+ }
8441
8616
  function configureBrokerKitPlugin(openclawConfig, config2) {
8442
8617
  const plugins = object(openclawConfig, "plugins");
8443
8618
  const load = object(plugins, "load");
@@ -8576,7 +8751,7 @@ function uniqueStrings(value, required) {
8576
8751
  }
8577
8752
 
8578
8753
  // src/mlclaw-space-runtime/openai-credentials.ts
8579
- import { createCipheriv, createDecipheriv, hkdfSync, randomBytes as randomBytes4 } from "node:crypto";
8754
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes as randomBytes5 } from "node:crypto";
8580
8755
  import fs2 from "node:fs/promises";
8581
8756
  import path2 from "node:path";
8582
8757
  function openAiConfigured(env = process.env) {
@@ -8645,7 +8820,7 @@ var OpenAiCredentialStore = class {
8645
8820
  if (!normalized) {
8646
8821
  throw new Error("valid OpenAI API key is required");
8647
8822
  }
8648
- const iv = randomBytes4(12);
8823
+ const iv = randomBytes5(12);
8649
8824
  const cipher = createCipheriv("aes-256-gcm", this.key, iv);
8650
8825
  const ciphertext = Buffer.concat([cipher.update(normalized, "utf8"), cipher.final()]);
8651
8826
  const envelope = {
@@ -8656,7 +8831,7 @@ var OpenAiCredentialStore = class {
8656
8831
  ciphertext: ciphertext.toString("base64url")
8657
8832
  };
8658
8833
  const directory = path2.dirname(this.file);
8659
- const temporary = `${this.file}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
8834
+ const temporary = `${this.file}.${process.pid}.${randomBytes5(6).toString("hex")}.tmp`;
8660
8835
  await fs2.mkdir(directory, { recursive: true, mode: 448 });
8661
8836
  try {
8662
8837
  await fs2.writeFile(temporary, `${JSON.stringify(envelope)}
@@ -8942,7 +9117,7 @@ function positiveNumber2(value) {
8942
9117
  }
8943
9118
 
8944
9119
  // src/mlclaw-space-runtime/cookies.ts
8945
- import { createHmac as createHmac4, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
9120
+ import { createHmac as createHmac4, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
8946
9121
  function createSignedCookie(options, payload) {
8947
9122
  const body = Buffer.from(JSON.stringify({
8948
9123
  ...payload,
@@ -8991,7 +9166,7 @@ function clearCookie(name, secure) {
8991
9166
  });
8992
9167
  }
8993
9168
  function randomState() {
8994
- return randomBytes5(24).toString("base64url");
9169
+ return randomBytes6(24).toString("base64url");
8995
9170
  }
8996
9171
  function sign2(value, secret) {
8997
9172
  return createHmac4("sha256", secret).update(value).digest("base64url");
@@ -9084,6 +9259,10 @@ function normalizeNext(value) {
9084
9259
  var SHELL_MARKER = "data-mlclaw-shell";
9085
9260
  var BRANDING_MARKER = "data-mlclaw-branding";
9086
9261
  var CONTROL_BRANDING_MARKER = "data-mlclaw-control-branding";
9262
+ var BROKERKIT_DELEGATED_UI_BOOTSTRAP = Buffer.from(
9263
+ JSON.stringify({ version: 1, mode: "delegated-web", basePath: "/mlclaw/api/brokerkit" }),
9264
+ "utf8"
9265
+ ).toString("base64url");
9087
9266
  var CONTROL_BRANDING_SCRIPT_PATH = "/assets/mlclaw-control-branding.js";
9088
9267
  var CONTROL_BRANDING_SCRIPT = `(function () {
9089
9268
  var productName = "ML Claw";
@@ -9152,31 +9331,125 @@ var CONTROL_BRANDING_SCRIPT = `(function () {
9152
9331
  }
9153
9332
  });
9154
9333
  }
9155
- function brokerKitFrameIn(root, source) {
9156
- if (!root.querySelectorAll) return;
9157
- var frames = root.querySelectorAll("iframe");
9158
- for (var i = 0; i < frames.length; i++) {
9159
- try {
9160
- var frameUrl = new URL(frames[i].src, location.href);
9161
- if (frames[i].contentWindow === source && frameUrl.origin === location.origin &&
9162
- frameUrl.pathname === "/plugins/brokerkit/ui/" && !frameUrl.search) return frames[i];
9163
- } catch (_) {}
9334
+ function installApprovals() {
9335
+ var shell = document.querySelector("[data-mlclaw-shell]");
9336
+ var button = document.querySelector("[data-mlclaw-approvals-button]");
9337
+ var popover = document.querySelector("[data-mlclaw-approvals-popover]");
9338
+ var frame = document.querySelector("[data-mlclaw-approvals-frame]");
9339
+ var badge = document.querySelector("[data-mlclaw-approvals-badge]");
9340
+ var close = document.querySelector("[data-mlclaw-approvals-close]");
9341
+ if (!shell || !button || !popover || !frame || button.getAttribute("data-ready") === "1") return;
9342
+ button.setAttribute("data-ready", "1");
9343
+ function invalidateFrame() {
9344
+ if (frame.contentWindow) {
9345
+ frame.contentWindow.postMessage({ type: "brokerkit.operator-ui.invalidate", version: 1 }, "*");
9346
+ }
9347
+ }
9348
+ function setOpen(open) {
9349
+ popover.hidden = !open;
9350
+ button.setAttribute("aria-expanded", open ? "true" : "false");
9351
+ if (!open) return;
9352
+ if (!frame.getAttribute("src")) frame.setAttribute("src", frame.getAttribute("data-src"));
9353
+ else invalidateFrame();
9354
+ }
9355
+ frame.addEventListener("load", function () { if (!popover.hidden) invalidateFrame(); });
9356
+ button.addEventListener("click", function () { setOpen(popover.hidden); });
9357
+ if (close) close.addEventListener("click", function () { setOpen(false); });
9358
+ document.addEventListener("click", function (event) {
9359
+ if (!popover.hidden && !shell.contains(event.target)) setOpen(false);
9360
+ });
9361
+ document.addEventListener("keydown", function (event) {
9362
+ if (event.key === "Escape") setOpen(false);
9363
+ });
9364
+ window.addEventListener("message", function (event) {
9365
+ var data = event.data;
9366
+ if (
9367
+ event.source !== frame.contentWindow ||
9368
+ !data ||
9369
+ typeof data !== "object" ||
9370
+ Object.keys(data).sort().join(",") !== "nonce,type,version" ||
9371
+ data.type !== "brokerkit.delegated-web.open" ||
9372
+ data.version !== 1 ||
9373
+ typeof data.nonce !== "string" ||
9374
+ !/^[a-f0-9]{32}$/.test(data.nonce)
9375
+ ) return;
9376
+ window.location.assign("/plugins/brokerkit/ui/#${BROKERKIT_DELEGATED_UI_BOOTSTRAP}");
9377
+ });
9378
+ var summaryCursor = "";
9379
+ var stopped = false;
9380
+ function acceptSummary(summary) {
9381
+ if (
9382
+ !summary ||
9383
+ typeof summary !== "object" ||
9384
+ Object.keys(summary).sort().join(",") !== "api_version,cursor,healthy,pending" ||
9385
+ summary.api_version !== "brokerkit.io/operator-ui/v1" ||
9386
+ typeof summary.cursor !== "string" ||
9387
+ summary.cursor.length < 1 ||
9388
+ summary.cursor.length > 128 ||
9389
+ typeof summary.pending !== "number" ||
9390
+ !Number.isSafeInteger(summary.pending) ||
9391
+ summary.pending < 0 ||
9392
+ typeof summary.healthy !== "boolean"
9393
+ ) return false;
9394
+ var changed = summaryCursor && summaryCursor !== summary.cursor;
9395
+ summaryCursor = summary.cursor;
9396
+ if (badge) {
9397
+ badge.textContent = summary.pending > 99 ? "99+" : String(summary.pending);
9398
+ badge.hidden = summary.pending < 1;
9399
+ }
9400
+ button.setAttribute("aria-label", summary.pending > 0 ? "Open approval requests (" + summary.pending + " pending)" : "Open approval requests");
9401
+ if (changed) invalidateFrame();
9402
+ return true;
9164
9403
  }
9165
- var elements = root.querySelectorAll("*");
9166
- for (var j = 0; j < elements.length; j++) {
9167
- if (elements[j].shadowRoot) {
9168
- var nested = brokerKitFrameIn(elements[j].shadowRoot, source);
9169
- if (nested) return nested;
9404
+ function refresh() {
9405
+ return fetch("/mlclaw/api/brokerkit/summary", { credentials: "same-origin", cache: "no-store" })
9406
+ .then(function (response) { return response.ok ? response.json() : null; })
9407
+ .then(acceptSummary)
9408
+ .catch(function () { return false; });
9409
+ }
9410
+ function watch(delay) {
9411
+ if (stopped) return;
9412
+ if (!summaryCursor) {
9413
+ refresh().then(function () { window.setTimeout(function () { watch(250); }, delay); });
9414
+ return;
9170
9415
  }
9416
+ fetch("/mlclaw/api/brokerkit/summary/events?cursor=" + encodeURIComponent(summaryCursor) + "&wait_seconds=25", {
9417
+ credentials: "same-origin",
9418
+ cache: "no-store"
9419
+ }).then(function (response) {
9420
+ if (response.status === 410) {
9421
+ summaryCursor = "";
9422
+ return null;
9423
+ }
9424
+ if (!response.ok) throw new Error("summary unavailable");
9425
+ return response.json();
9426
+ }).then(function (event) {
9427
+ if (
9428
+ event &&
9429
+ typeof event === "object" &&
9430
+ Object.keys(event).sort().join(",") === "api_version,changed,cursor" &&
9431
+ event.api_version === "brokerkit.io/operator-ui/v1" &&
9432
+ typeof event.cursor === "string" &&
9433
+ event.cursor.length >= 1 &&
9434
+ event.cursor.length <= 128 &&
9435
+ typeof event.changed === "boolean"
9436
+ ) {
9437
+ summaryCursor = event.cursor;
9438
+ if (event.changed) invalidateFrame();
9439
+ return event.changed ? refresh() : true;
9440
+ }
9441
+ return false;
9442
+ }).then(function (ok) {
9443
+ window.setTimeout(function () { watch(ok ? 250 : Math.min(delay * 2, 30000)); }, ok ? 0 : delay);
9444
+ }).catch(function () {
9445
+ window.setTimeout(function () { watch(Math.min(delay * 2, 30000)); }, delay);
9446
+ });
9171
9447
  }
9448
+ refresh().then(function () { watch(250); });
9449
+ window.setInterval(refresh, 300000);
9450
+ window.addEventListener("focus", function () { refresh(); });
9451
+ window.addEventListener("beforeunload", function () { stopped = true; });
9172
9452
  }
9173
- window.addEventListener("message", function (event) {
9174
- var message = event.data;
9175
- if (event.origin !== "null" || !message || message.type !== "brokerkit.delegated-web.open" ||
9176
- message.version !== 1 || typeof message.nonce !== "string" || !/^[a-f0-9]{32}$/.test(message.nonce)) return;
9177
- var frame = brokerKitFrameIn(document, event.source);
9178
- if (frame) location.assign(frame.src);
9179
- });
9180
9453
  if (!document.documentElement.hasAttribute(marker)) {
9181
9454
  document.documentElement.setAttribute(marker, "1");
9182
9455
  var attachShadow = Element.prototype.attachShadow;
@@ -9190,6 +9463,7 @@ var CONTROL_BRANDING_SCRIPT = `(function () {
9190
9463
  requestAnimationFrame(function () {
9191
9464
  observeExistingShadowRoots(document);
9192
9465
  scan(document);
9466
+ installApprovals();
9193
9467
  });
9194
9468
  }
9195
9469
  })();
@@ -9222,6 +9496,13 @@ function rewriteOpenClawHtml(html, branding) {
9222
9496
  function injectMlClawShell(html, branding) {
9223
9497
  const shell = `
9224
9498
  <div ${SHELL_MARKER} style="position:fixed;left:max(12px,env(safe-area-inset-left));bottom:max(12px,env(safe-area-inset-bottom));z-index:2147483647;">
9499
+ <section data-mlclaw-approvals-popover hidden aria-label="Approval requests" style="position:absolute;left:0;bottom:44px;box-sizing:border-box;width:min(420px,calc(100vw - 24px));height:min(620px,calc(100dvh - 72px));overflow:hidden;border:1px solid rgba(15,23,42,.16);border-radius:14px;background:white;box-shadow:0 18px 48px rgba(15,23,42,.24);">
9500
+ <header style="box-sizing:border-box;display:flex;height:42px;align-items:center;justify-content:space-between;padding:0 10px 0 14px;border-bottom:1px solid rgba(15,23,42,.1);color:#111827;font:600 14px system-ui;">
9501
+ <span>Approvals</span>
9502
+ <button data-mlclaw-approvals-close type="button" aria-label="Close approval requests" style="display:grid;width:30px;height:30px;place-items:center;border:0;border-radius:7px;background:transparent;color:#475569;cursor:pointer;font:20px/1 system-ui;">&times;</button>
9503
+ </header>
9504
+ <iframe data-mlclaw-approvals-frame data-src="/plugins/brokerkit/ui/?embed=popover#${BROKERKIT_DELEGATED_UI_BOOTSTRAP}" title="Approval requests" sandbox="allow-scripts" style="display:block;width:100%;height:calc(100% - 42px);border:0;background:white;"></iframe>
9505
+ </section>
9225
9506
  <div style="display:flex;gap:8px;align-items:center;">
9226
9507
  <a href="/mlclaw" aria-label="Open ${escapeHtml2(branding.name)} settings" title="${escapeHtml2(branding.name)}" style="box-sizing:border-box;display:flex;width:34px;height:34px;aspect-ratio:1/1;align-items:center;justify-content:center;border:1px solid rgba(15,23,42,.16);border-radius:8px;background:rgba(255,255,255,.94);box-shadow:0 8px 18px rgba(15,23,42,.14);color:#111827;text-decoration:none;">
9227
9508
  <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;width:18px;height:18px;">
@@ -9229,6 +9510,10 @@ function injectMlClawShell(html, branding) {
9229
9510
  <circle cx="12" cy="12" r="3"></circle>
9230
9511
  </svg>
9231
9512
  </a>
9513
+ <button data-mlclaw-approvals-button type="button" aria-label="Open approval requests" aria-expanded="false" style="position:relative;box-sizing:border-box;display:grid;width:34px;height:34px;place-items:center;border:1px solid rgba(15,23,42,.16);border-radius:8px;background:rgba(255,255,255,.94);box-shadow:0 8px 18px rgba(15,23,42,.14);color:#111827;cursor:pointer;">
9514
+ <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"></path><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"></path></svg>
9515
+ <span data-mlclaw-approvals-badge hidden style="position:absolute;place-items:center;min-width:17px;height:17px;right:-6px;top:-7px;padding:0 4px;border:2px solid white;border-radius:999px;background:#dc2626;color:white;font:700 9px system-ui;"></span>
9516
+ </button>
9232
9517
  </div>
9233
9518
  </div>
9234
9519
  `;
@@ -9266,12 +9551,16 @@ function escapeHtml2(value) {
9266
9551
  }
9267
9552
 
9268
9553
  // src/mlclaw-space-runtime/app.ts
9554
+ var BROKERKIT_SESSION_HEADER = "brokerkit-session";
9269
9555
  function createSpaceRuntimeApp(config2, controls) {
9270
9556
  const app = new Hono2();
9271
9557
  const operatorBrokers = new OperatorBrokerRegistry(config2.operatorBrokers);
9272
9558
  const delegatedBrokerKit = new DelegatedBrokerKit(operatorBrokers, config2.sessionSecret);
9273
9559
  const allowDelegatedSessionSnapshot = fixedWindowRateLimit(12, 6e4);
9274
9560
  const allowDelegatedActorSnapshot = fixedWindowRateLimit(60, 6e4);
9561
+ const allowBrokerKitSummary = fixedWindowRateLimit(12, 6e4);
9562
+ const allowDelegatedEvents = fixedWindowRateLimit(60, 6e4);
9563
+ const allowSummaryEvents = fixedWindowRateLimit(60, 6e4);
9275
9564
  const openAiCredentials = new OpenAiCredentialStore(config2.openaiCredentialStoreFile, config2.credentialKey);
9276
9565
  app.get("/health", (c) => health(c, config2, controls));
9277
9566
  app.get("/healthz", (c) => health(c, config2, controls));
@@ -9341,11 +9630,33 @@ function createSpaceRuntimeApp(config2, controls) {
9341
9630
  }
9342
9631
  return c.json(await statusPayload(config2, controls));
9343
9632
  });
9633
+ app.get("/mlclaw/api/brokerkit/summary", async (c) => {
9634
+ const auth = requireAdmin(c, config2);
9635
+ if (auth instanceof Response) return auth;
9636
+ if (!allowBrokerKitSummary(auth.username)) return c.json({ ok: false, error: "rate limited" }, 429);
9637
+ try {
9638
+ return c.json(await delegatedBrokerKit.summary());
9639
+ } catch {
9640
+ return c.json({ ok: false, error: "operator inbox unavailable" }, 503);
9641
+ }
9642
+ });
9643
+ app.get("/mlclaw/api/brokerkit/summary/events", async (c) => {
9644
+ const auth = requireAdmin(c, config2);
9645
+ if (auth instanceof Response) return auth;
9646
+ if (!allowSummaryEvents(auth.username)) return c.json({ ok: false, error: "rate limited" }, 429);
9647
+ const input = delegatedEventQuery(c.req.url);
9648
+ if (!input) return c.json({ ok: false, error: "invalid request" }, 400);
9649
+ try {
9650
+ return c.json(await delegatedBrokerKit.events(input.cursor, input.waitSeconds, c.req.raw.signal));
9651
+ } catch (error) {
9652
+ return delegatedFailure(c, error);
9653
+ }
9654
+ });
9344
9655
  app.options("/mlclaw/api/brokerkit/*", (c) => delegatedPreflight(c));
9345
9656
  app.post("/mlclaw/api/brokerkit/session", (c) => {
9346
9657
  const identity = delegatedIdentity(c, delegatedBrokerKit);
9347
9658
  if (!identity) return delegatedErrorResponse(c, "not_authorized", 401);
9348
- return delegatedJson(c, delegatedBrokerKit.issueSession(identity.actor));
9659
+ return delegatedJson(c, delegatedBrokerKit.issueSession(identity.actor, identity.access));
9349
9660
  });
9350
9661
  app.get("/mlclaw/api/brokerkit/snapshot", async (c) => {
9351
9662
  const identity = delegatedIdentity(c, delegatedBrokerKit);
@@ -9359,6 +9670,18 @@ function createSpaceRuntimeApp(config2, controls) {
9359
9670
  return delegatedFailure(c, error);
9360
9671
  }
9361
9672
  });
9673
+ app.get("/mlclaw/api/brokerkit/events", async (c) => {
9674
+ const identity = delegatedIdentity(c, delegatedBrokerKit);
9675
+ if (!identity) return delegatedErrorResponse(c, "not_authorized", 401);
9676
+ if (!allowDelegatedEvents(identity.sessionId)) return delegatedErrorResponse(c, "rate_limited", 429);
9677
+ const input = delegatedEventQuery(c.req.url);
9678
+ if (!input) return delegatedErrorResponse(c, "invalid_input", 400);
9679
+ try {
9680
+ return delegatedJson(c, await delegatedBrokerKit.events(input.cursor, input.waitSeconds, c.req.raw.signal));
9681
+ } catch (error) {
9682
+ return delegatedFailure(c, error);
9683
+ }
9684
+ });
9362
9685
  app.get("/mlclaw/api/brokerkit/requests/:handle", async (c) => {
9363
9686
  const identity = delegatedIdentity(c, delegatedBrokerKit);
9364
9687
  if (!identity) return delegatedErrorResponse(c, "not_authorized", 401);
@@ -9368,28 +9691,27 @@ function createSpaceRuntimeApp(config2, controls) {
9368
9691
  return delegatedFailure(c, error);
9369
9692
  }
9370
9693
  });
9371
- for (const action of ["approve", "deny", "cancel", "revoke"]) {
9694
+ for (const action of ["approve", "deny", "revoke"]) {
9372
9695
  app.post(`/mlclaw/api/brokerkit/requests/:handle/${action}`, async (c) => {
9373
9696
  const identity = delegatedIdentity(c, delegatedBrokerKit);
9374
- if (!identity) return delegatedErrorResponse(c, "not_authorized", 401);
9697
+ if (!identity || identity.access !== "decide") return delegatedErrorResponse(c, "not_authorized", 401);
9375
9698
  const body = await readBoundedJson(c, 16384);
9376
- if (!body || Object.keys(body).some((key) => !["expectedRevision", "reason", "constraints"].includes(key))) {
9699
+ if (!body || Object.keys(body).some((key) => !["expectedRevision", "constraints"].includes(key))) {
9377
9700
  return delegatedErrorResponse(c, "invalid_input", 400);
9378
9701
  }
9379
9702
  const constraints = recordValue(body.constraints);
9380
9703
  const expectedRevision = positiveJsonInteger(body.expectedRevision);
9381
9704
  const durationSeconds = optionalPositiveJsonInteger(constraints?.durationSeconds);
9382
- const maxUses = optionalPositiveJsonInteger(constraints?.maxUses);
9383
- if (!expectedRevision || body.reason !== void 0 && (typeof body.reason !== "string" || body.reason.length > 2e3) || body.constraints !== void 0 && (!constraints || Object.keys(constraints).some((key) => !["durationSeconds", "maxUses"].includes(key)) || durationSeconds === void 0 || maxUses === void 0) || durationSeconds === "invalid" || maxUses === "invalid" || action !== "approve" && (durationSeconds !== void 0 || maxUses !== void 0)) {
9705
+ const maxUses = optionalUseLimitJsonInteger(constraints?.maxUses);
9706
+ if (!expectedRevision || body.constraints !== void 0 && (!constraints || Object.keys(constraints).some((key) => !["durationSeconds", "maxUses"].includes(key)) || durationSeconds === void 0 || maxUses === void 0) || durationSeconds === "invalid" || maxUses === "invalid" || action !== "approve" && (durationSeconds !== void 0 || maxUses !== void 0)) {
9384
9707
  return delegatedErrorResponse(c, "invalid_input", 400);
9385
9708
  }
9386
9709
  try {
9387
9710
  return delegatedJson(
9388
9711
  c,
9389
9712
  await delegatedBrokerKit.decide(c.req.param("handle"), action, expectedRevision, identity.actor, {
9390
- ...typeof body.reason === "string" ? { reason: body.reason } : {},
9391
9713
  ...typeof durationSeconds === "number" ? { durationSeconds } : {},
9392
- ...typeof maxUses === "number" ? { maxUses } : {}
9714
+ ...typeof maxUses === "number" || maxUses === null ? { maxUses } : {}
9393
9715
  })
9394
9716
  );
9395
9717
  } catch (error) {
@@ -9397,6 +9719,7 @@ function createSpaceRuntimeApp(config2, controls) {
9397
9719
  }
9398
9720
  });
9399
9721
  }
9722
+ app.all("/mlclaw/api/brokerkit/*", (c) => delegatedErrorResponse(c, "not_found", 404));
9400
9723
  app.post("/mlclaw/api/integrations/huggingface/disconnect", async (c) => {
9401
9724
  const auth = requireAdmin(c, config2);
9402
9725
  if (auth instanceof Response) {
@@ -9668,16 +9991,28 @@ async function trustedBrokerKitUi(c, config2, delegatedBrokerKit) {
9668
9991
  if (relative === "index.html") {
9669
9992
  const destination = c.req.header("sec-fetch-dest");
9670
9993
  if (destination !== "iframe" && destination !== "document") return c.text("not found\n", 404);
9994
+ const query = new URL(c.req.url).search;
9995
+ const embeddedPopover = destination === "iframe" && query === "?embed=popover";
9996
+ if (query && !embeddedPopover) return c.text("not found\n", 404);
9671
9997
  const auth = requireAdmin(c, config2);
9672
9998
  if (auth instanceof Response) return auth;
9673
9999
  try {
9674
10000
  const template = await fs3.readFile(file, "utf8");
9675
- const marker = destination === "iframe" ? '<meta name="brokerkit-delegated-top-level">' : `<meta name="brokerkit-delegated-session" content="${Buffer.from(
9676
- JSON.stringify(delegatedBrokerKit.issueSession(auth.username)),
10001
+ const delegatedSession = destination === "document" || embeddedPopover;
10002
+ const marker = !delegatedSession ? '<meta name="brokerkit-delegated-top-level">' : `<meta name="brokerkit-delegated-session" content="${Buffer.from(
10003
+ JSON.stringify(
10004
+ delegatedBrokerKit.issueSession(
10005
+ auth.username,
10006
+ embeddedPopover && !config2.brokerKitPopoverDecisions ? "read" : "decide"
10007
+ )
10008
+ ),
9677
10009
  "utf8"
9678
10010
  ).toString("base64url")}">`;
9679
10011
  if (!template.includes("</head>")) return c.text("not found\n", 404);
9680
- const headers2 = trustedBrokerKitHeaders(destination === "iframe" ? "launcher" : "top-level");
10012
+ const headers2 = trustedBrokerKitHeaders(
10013
+ embeddedPopover ? "popover" : destination === "iframe" ? "launcher" : "top-level",
10014
+ new URL(c.req.url).origin
10015
+ );
9681
10016
  headers2.set("content-type", "text/html; charset=utf-8");
9682
10017
  return new Response(template.replace("</head>", `${marker}</head>`), { status: 200, headers: headers2 });
9683
10018
  } catch {
@@ -9686,15 +10021,16 @@ async function trustedBrokerKitUi(c, config2, delegatedBrokerKit) {
9686
10021
  }
9687
10022
  const response = await serveFile(file, contentType(file), true);
9688
10023
  if (response.status !== 200) return response;
9689
- const headers = trustedBrokerKitHeaders("asset");
10024
+ const headers = trustedBrokerKitHeaders("asset", new URL(c.req.url).origin);
9690
10025
  headers.set("content-type", response.headers.get("content-type") ?? "application/octet-stream");
9691
10026
  return new Response(response.body, { status: response.status, headers });
9692
10027
  }
9693
- function trustedBrokerKitHeaders(mode) {
10028
+ function trustedBrokerKitHeaders(mode, origin) {
9694
10029
  const asset = mode === "asset";
10030
+ const sandbox = mode === "top-level" || mode === "popover" ? "sandbox allow-scripts; " : "";
9695
10031
  const headers = new Headers({
9696
10032
  "cache-control": asset ? "public, max-age=31536000, immutable" : "no-store",
9697
- "content-security-policy": `sandbox allow-scripts; default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors ${mode === "top-level" ? "'none'" : "'self'"}`,
10033
+ "content-security-policy": `${sandbox}default-src 'self'; script-src 'self' ${origin}; style-src 'self' 'unsafe-inline' ${origin}; connect-src 'self' ${origin}; img-src 'self' data:; frame-ancestors ${mode === "top-level" ? "'none'" : "'self'"}`,
9698
10034
  "cross-origin-resource-policy": asset ? "cross-origin" : "same-origin",
9699
10035
  "referrer-policy": "no-referrer",
9700
10036
  "x-content-type-options": "nosniff",
@@ -9747,14 +10083,24 @@ function requireCsrf(c, config2, username) {
9747
10083
  function delegatedOriginAllowed(c) {
9748
10084
  return c.req.header("origin") === "null";
9749
10085
  }
10086
+ function delegatedEventQuery(urlValue) {
10087
+ const url = new URL(urlValue);
10088
+ if ([...url.searchParams.keys()].some((key) => key !== "cursor" && key !== "wait_seconds")) return void 0;
10089
+ const cursor = url.searchParams.get("cursor") ?? "";
10090
+ const wait = url.searchParams.get("wait_seconds") ?? "25";
10091
+ if (cursor.length < 1 || cursor.length > 128 || !/^[A-Za-z0-9_.-]+$/u.test(cursor) || !/^(?:[1-9]|1[0-9]|2[0-5])$/u.test(wait)) {
10092
+ return void 0;
10093
+ }
10094
+ return { cursor, waitSeconds: Number(wait) };
10095
+ }
9750
10096
  function delegatedIdentity(c, delegated) {
9751
10097
  if (!delegatedOriginAllowed(c)) return void 0;
9752
- return delegated.authorizeSession(c.req.header("authorization"));
10098
+ return delegated.authorizeSession(c.req.header(BROKERKIT_SESSION_HEADER));
9753
10099
  }
9754
10100
  function delegatedPreflight(c) {
9755
10101
  if (!delegatedOriginAllowed(c)) return delegatedErrorResponse(c, "not_authorized", 403);
9756
10102
  delegatedHeaders(c);
9757
- c.header("access-control-allow-headers", "authorization, content-type");
10103
+ c.header("access-control-allow-headers", `${BROKERKIT_SESSION_HEADER}, content-type`);
9758
10104
  c.header("access-control-allow-methods", "GET, POST, OPTIONS");
9759
10105
  c.header("access-control-max-age", "300");
9760
10106
  return c.body(null, 204);
@@ -9769,7 +10115,7 @@ function delegatedErrorResponse(c, code, status) {
9769
10115
  }
9770
10116
  function delegatedFailure(c, error) {
9771
10117
  if (error instanceof DelegatedBrokerKitError) {
9772
- const status = error.code === "request_not_found" ? 404 : error.code === "revision_stale" || error.code === "action_not_allowed" ? 409 : 502;
10118
+ const status = error.code === "request_not_found" ? 404 : error.code === "cursor_expired" ? 410 : error.code === "revision_stale" || error.code === "action_not_allowed" ? 409 : 502;
9773
10119
  return delegatedErrorResponse(c, error.code, status);
9774
10120
  }
9775
10121
  if (error instanceof BrokerOperatorError) {
@@ -9777,13 +10123,22 @@ function delegatedFailure(c, error) {
9777
10123
  const status = error.status === 404 ? 404 : error.status === 409 ? 409 : 502;
9778
10124
  return delegatedErrorResponse(c, code, status);
9779
10125
  }
9780
- process.stderr.write(`[mlclaw] delegated BrokerKit request failed: ${formatError(error)}
9781
- `);
10126
+ process.stderr.write(
10127
+ `[mlclaw] delegated BrokerKit request failed: route=${delegatedRouteLabel(c)} status=502 class=${safeErrorClass(error)}
10128
+ `
10129
+ );
9782
10130
  return delegatedErrorResponse(c, "source_unavailable", 502);
9783
10131
  }
10132
+ function delegatedRouteLabel(c) {
10133
+ const pathLabel = c.req.path.replace(/\/requests\/[^/]+/u, "/requests/:handle");
10134
+ return `${c.req.method}:${pathLabel}`;
10135
+ }
10136
+ function safeErrorClass(error) {
10137
+ const name = error instanceof Error ? error.name : typeof error;
10138
+ return /^[A-Za-z][A-Za-z0-9]{0,79}$/u.test(name) ? name : "unknown";
10139
+ }
9784
10140
  function delegatedHeaders(c) {
9785
10141
  c.header("access-control-allow-origin", "null");
9786
- c.header("access-control-allow-credentials", "true");
9787
10142
  c.header("cache-control", "no-store");
9788
10143
  c.header("vary", "origin");
9789
10144
  c.header("x-content-type-options", "nosniff");
@@ -9960,6 +10315,10 @@ function optionalPositiveJsonInteger(value) {
9960
10315
  if (value === void 0) return void 0;
9961
10316
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : "invalid";
9962
10317
  }
10318
+ function optionalUseLimitJsonInteger(value) {
10319
+ if (value === null) return null;
10320
+ return optionalPositiveJsonInteger(value);
10321
+ }
9963
10322
  function fixedWindowRateLimit(limit, windowMs) {
9964
10323
  const windows = /* @__PURE__ */ new Map();
9965
10324
  return (key) => {
@@ -10066,7 +10425,7 @@ function contentType(file) {
10066
10425
  }
10067
10426
 
10068
10427
  // src/mlclaw-space-runtime/mcp-credentials.ts
10069
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, hkdfSync as hkdfSync2, randomBytes as randomBytes6 } from "node:crypto";
10428
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, hkdfSync as hkdfSync2, randomBytes as randomBytes7 } from "node:crypto";
10070
10429
  import fs4 from "node:fs/promises";
10071
10430
  import path4 from "node:path";
10072
10431
  var DEFAULT_REFRESH_TIMEOUT_MS = 3e4;
@@ -10199,7 +10558,7 @@ var McpCredentialStore = class {
10199
10558
  async persist() {
10200
10559
  const directory = path4.dirname(this.options.file);
10201
10560
  await fs4.mkdir(directory, { recursive: true, mode: 448 });
10202
- const temporary = `${this.options.file}.${process.pid}.${randomBytes6(6).toString("hex")}.tmp`;
10561
+ const temporary = `${this.options.file}.${process.pid}.${randomBytes7(6).toString("hex")}.tmp`;
10203
10562
  const encrypted = encryptDocument(this.document, this.key);
10204
10563
  try {
10205
10564
  await fs4.writeFile(temporary, `${JSON.stringify(encrypted)}
@@ -10277,7 +10636,7 @@ var InvalidCredentialFileError = class extends Error {
10277
10636
  }
10278
10637
  };
10279
10638
  function encryptDocument(document, key) {
10280
- const iv = randomBytes6(12);
10639
+ const iv = randomBytes7(12);
10281
10640
  const cipher = createCipheriv2("aes-256-gcm", key, iv);
10282
10641
  const plaintext = Buffer.from(JSON.stringify(document), "utf8");
10283
10642
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
@@ -10766,6 +11125,8 @@ var OPENCLAW_ENV_ALLOWLIST = [
10766
11125
  "OPENCLAW_LIVE_DIR",
10767
11126
  "OPENCLAW_STATE_DIR",
10768
11127
  "OPENCLAW_WORKSPACE_DIR",
11128
+ "MLCLAW_HF_BROKER_URL",
11129
+ "MLCLAW_HF_BROKER_AGENT_SECRET_FILE",
10769
11130
  "TELEGRAM_API_ROOT",
10770
11131
  "TELEGRAM_ALLOWED_USERS",
10771
11132
  "TELEGRAM_BOT_TOKEN",