@blotout/qinto-cli 0.1.8 → 0.1.10

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.
@@ -7,6 +7,10 @@ var __export = (target, all) => {
7
7
  // ../../../libs/app-manifest/src/cron.ts
8
8
  var MAX_CRON_LENGTH = 128;
9
9
 
10
+ // ../../../libs/app-manifest/src/tool-names.ts
11
+ var MAX_APP_TOOL_NAME_LENGTH = 32;
12
+ var APP_TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
13
+
10
14
  // ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
11
15
  var external_exports = {};
12
16
  __export(external_exports, {
@@ -2348,13 +2352,13 @@ var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
2348
2352
  canary.aborted = true;
2349
2353
  return canary;
2350
2354
  }
2351
- const checkResult = runChecks(payload, checks, ctx);
2352
- if (checkResult instanceof Promise) {
2355
+ const checkResult2 = runChecks(payload, checks, ctx);
2356
+ if (checkResult2 instanceof Promise) {
2353
2357
  if (ctx.async === false)
2354
2358
  throw new $ZodAsyncError();
2355
- return checkResult.then((checkResult2) => inst._zod.parse(checkResult2, ctx));
2359
+ return checkResult2.then((checkResult3) => inst._zod.parse(checkResult3, ctx));
2356
2360
  }
2357
- return inst._zod.parse(checkResult, ctx);
2361
+ return inst._zod.parse(checkResult2, ctx);
2358
2362
  };
2359
2363
  inst._zod.run = (payload, ctx) => {
2360
2364
  if (ctx.skipChecks) {
@@ -3036,10 +3040,10 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3036
3040
  const shape = def.shape;
3037
3041
  const propValues = {};
3038
3042
  for (const key in shape) {
3039
- const field = shape[key]._zod;
3040
- if (field.values) {
3043
+ const field2 = shape[key]._zod;
3044
+ if (field2.values) {
3041
3045
  propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());
3042
- for (const v of field.values)
3046
+ for (const v of field2.values)
3043
3047
  propValues[key].add(v);
3044
3048
  }
3045
3049
  }
@@ -14628,6 +14632,11 @@ var envelopeResponseSchema = external_exports.object({
14628
14632
  var CONTROL_ENVELOPE_VERSION = 1;
14629
14633
  var MAX_CONTROL_BYTES = 256 * 1024;
14630
14634
  var MAX_MIGRATION_CHECKPOINT_BYTES = 16 * 1024;
14635
+ var MAX_TOOL_ARGUMENTS_BYTES = 32 * 1024;
14636
+ var MAX_TOOL_RESULT_BYTES = 64 * 1024;
14637
+ var MAX_TOOL_DATA_DEPTH = 8;
14638
+ var MAX_TOOL_DATA_NODES = 5e3;
14639
+ var MAX_TOOL_ERROR_MESSAGE_LENGTH = 512;
14631
14640
  var installSchema2 = external_exports.object({
14632
14641
  id: external_exports.string().min(1).max(64),
14633
14642
  ord: external_exports.number().int().min(1),
@@ -14701,15 +14710,32 @@ var scheduleSchema = external_exports.object({
14701
14710
  }).strict()
14702
14711
  ).min(1).max(MAX_INSTALLS_PER_APP)
14703
14712
  }).strict();
14713
+ var toolSchema = external_exports.object({
14714
+ // The manifest's tools[].name, never the `<slug>_` composed MCP name: the slug is already in
14715
+ // `app.slug`, and carrying it twice would give the app worker two sources for one fact.
14716
+ name: external_exports.string().min(1).max(MAX_APP_TOOL_NAME_LENGTH).regex(APP_TOOL_NAME_PATTERN),
14717
+ // Who asked, and what they may do on this site. Derived from the bearer by the portal, never
14718
+ // from the model's arguments.
14719
+ viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
14720
+ // Already validated against the tool's declared inputSchema, with the reserved routing names
14721
+ // stripped. Every declared input schema has an object root, so this is an object too — and a
14722
+ // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
14723
+ arguments: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
14724
+ // Stable across a collapsed retry, so an app can deduplicate its external effects on it.
14725
+ callId: external_exports.string().min(1).max(64)
14726
+ }).strict().refine((tool) => {
14727
+ return new TextEncoder().encode(JSON.stringify(tool.arguments)).length <= MAX_TOOL_ARGUMENTS_BYTES;
14728
+ }, "Tool arguments exceed the size cap");
14704
14729
  var controlEnvelopeSchema = external_exports.object({
14705
14730
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
14706
- kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule"]),
14731
+ kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule", "tool"]),
14707
14732
  site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
14708
14733
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
14709
14734
  install: external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100) }).strict().optional(),
14710
14735
  lifecycle: lifecycleEventSchema.optional(),
14711
14736
  migration: migrationSchema.optional(),
14712
- schedule: scheduleSchema.optional()
14737
+ schedule: scheduleSchema.optional(),
14738
+ tool: toolSchema.optional()
14713
14739
  }).strict().superRefine((envelope, ctx) => {
14714
14740
  if (envelope.kind === "lifecycle" !== (envelope.lifecycle !== void 0)) {
14715
14741
  ctx.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle payload must match the control kind" });
@@ -14720,13 +14746,50 @@ var controlEnvelopeSchema = external_exports.object({
14720
14746
  if (envelope.kind === "schedule" !== (envelope.schedule !== void 0)) {
14721
14747
  ctx.addIssue({ code: "custom", path: ["schedule"], message: "Schedule payload must match the control kind" });
14722
14748
  }
14749
+ if (envelope.kind === "tool" !== (envelope.tool !== void 0)) {
14750
+ ctx.addIssue({ code: "custom", path: ["tool"], message: "Tool payload must match the control kind" });
14751
+ }
14723
14752
  if (envelope.kind === "lifecycle" && envelope.install === void 0) {
14724
14753
  ctx.addIssue({ code: "custom", path: ["install"], message: "Lifecycle control needs an install" });
14725
14754
  }
14755
+ if (envelope.kind === "tool" && envelope.install === void 0) {
14756
+ ctx.addIssue({ code: "custom", path: ["install"], message: "Tool control needs an install" });
14757
+ }
14726
14758
  });
14759
+ var toolDataWithinLimits = (value) => {
14760
+ const stack = [{ value, depth: 0 }];
14761
+ let seen = 0;
14762
+ while (stack.length > 0) {
14763
+ const next = stack.pop();
14764
+ seen += 1;
14765
+ if (next.depth > MAX_TOOL_DATA_DEPTH || seen > MAX_TOOL_DATA_NODES) {
14766
+ return false;
14767
+ }
14768
+ if (next.value !== null && typeof next.value === "object") {
14769
+ for (const child of Object.values(next.value)) {
14770
+ stack.push({ value: child, depth: next.depth + 1 });
14771
+ }
14772
+ }
14773
+ }
14774
+ return true;
14775
+ };
14776
+ var toolResultSchema = external_exports.object({
14777
+ content: external_exports.string().max(MAX_TOOL_RESULT_BYTES),
14778
+ // MCP defines `structuredContent` as an object, so a bare scalar or array is refused here rather
14779
+ // than surfacing as a protocol error at the client.
14780
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
14781
+ // True when the handler chose to fail with `ctx.fail`: `content` is then the app's own message
14782
+ // for the model. A thrown handler is `ok: false` with no `tool` at all.
14783
+ failed: external_exports.boolean()
14784
+ }).strict().refine((result) => {
14785
+ return toolDataWithinLimits(result.data);
14786
+ }, "Tool result nests too deeply or carries too many values").refine((result) => {
14787
+ return new TextEncoder().encode(JSON.stringify({ content: result.content, data: result.data ?? null })).length <= MAX_TOOL_RESULT_BYTES;
14788
+ }, "Tool result exceeds the size cap");
14727
14789
  var controlResponseSchema = external_exports.object({
14728
14790
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
14729
14791
  ok: external_exports.boolean(),
14792
+ tool: toolResultSchema.optional(),
14730
14793
  migration: external_exports.union([
14731
14794
  external_exports.object({ done: external_exports.literal(true) }).strict(),
14732
14795
  external_exports.object({ done: external_exports.literal(false), checkpoint: external_exports.unknown() }).strict().refine((result) => {
@@ -14741,12 +14804,21 @@ var ENDPOINT_CONTEXT_HEADER = "x-qinto-endpoint-context";
14741
14804
  var ENDPOINT_OUTCOMES_HEADER = "x-qinto-endpoint-outcomes";
14742
14805
  var MAX_ENDPOINT_BODY_BYTES = 1024 * 1024;
14743
14806
  var MAX_ENDPOINT_OUTCOMES_BYTES = 64 * 1024;
14807
+ var MAX_ENDPOINT_PATH_LENGTH = 16 * 1024;
14744
14808
  var installSchema3 = external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100) }).strict();
14745
14809
  var endpointContextSchema = external_exports.object({
14746
- path: external_exports.string().min(1).max(255),
14810
+ // The declared template, which keys the bundle's handler map; `path` is what the caller asked
14811
+ // for. Only `route` may key the rate limiter — a concrete path gives every parameter value its
14812
+ // own bucket, which is no limit at all.
14813
+ route: external_exports.string().min(1).max(255),
14814
+ path: external_exports.string().min(1).max(MAX_ENDPOINT_PATH_LENGTH),
14815
+ // A name is cut from `route`, so the route's own cap bounds it; anything tighter would reject a
14816
+ // parameter the manifest grammar accepts and the worker already matched.
14817
+ params: external_exports.record(external_exports.string().min(1).max(255), external_exports.string().max(MAX_ENDPOINT_PATH_LENGTH)).default({}),
14747
14818
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
14748
14819
  mode: external_exports.enum(["webhook", "api"]),
14749
14820
  version: external_exports.string().min(1).max(32),
14821
+ site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
14750
14822
  installs: external_exports.array(installSchema3).min(1).max(15)
14751
14823
  }).strict().refine((context) => {
14752
14824
  return context.mode === "webhook" || context.installs.length === 1;
@@ -14761,6 +14833,96 @@ var endpointOutcomesSchema = external_exports.object({
14761
14833
  ).max(15)
14762
14834
  }).strict();
14763
14835
 
14836
+ // ../../../libs/app-envelope/src/oauth.ts
14837
+ var APP_OAUTH_BINDING_PREFIX = "OAUTH_";
14838
+ var appOauthBindingName = (providerId, ordinal) => {
14839
+ return `${APP_OAUTH_BINDING_PREFIX}${providerId.toUpperCase().replaceAll("-", "_")}_${ordinal}`;
14840
+ };
14841
+ var parseAppOauthBinding = (raw) => {
14842
+ if (raw === void 0 || raw === "") {
14843
+ return null;
14844
+ }
14845
+ let parsed;
14846
+ try {
14847
+ parsed = JSON.parse(raw);
14848
+ } catch {
14849
+ return null;
14850
+ }
14851
+ if (typeof parsed !== "object" || parsed === null) {
14852
+ return null;
14853
+ }
14854
+ const value = parsed;
14855
+ if (value["st"] !== "connected" && value["st"] !== "reconnect_required") {
14856
+ return null;
14857
+ }
14858
+ const expiresAt = value["e"];
14859
+ const token = value["t"];
14860
+ return {
14861
+ ...typeof token === "string" && token !== "" ? { t: token } : {},
14862
+ e: typeof expiresAt === "number" ? expiresAt : null,
14863
+ st: value["st"]
14864
+ };
14865
+ };
14866
+
14867
+ // ../../../libs/app-envelope/src/observability.ts
14868
+ var APP_OBSERVABILITY_HEADER = "x-qinto-app-observability";
14869
+ var APP_OBSERVABILITY_VERSION = 1;
14870
+ var MAX_APP_OBSERVABILITY_EVENTS = 20;
14871
+ var MAX_APP_SAVED_ERRORS = 5;
14872
+ var MAX_APP_EVENT_NAME_BYTES = 128;
14873
+ var MAX_APP_ACTION_NAME_BYTES = 256;
14874
+ var MAX_APP_ERROR_DESCRIPTION_BYTES = 2048;
14875
+ var MAX_APP_ERROR_PAYLOAD_BYTES = 4096;
14876
+ var MAX_APP_DATA_POINT_BLOB_BYTES = 16 * 1024;
14877
+ var MAX_APP_OBSERVABILITY_BYTES = 32 * 1024;
14878
+ var APP_OBSERVABILITY_JSON_BUDGET = MAX_APP_OBSERVABILITY_BYTES / 4 * 3;
14879
+ var APP_EVENT_STATUSES = ["success", "error"];
14880
+ var APP_ERROR_CATEGORIES = ["authorization", "configuration", "validation", "rate_limit", "upstream", "internal"];
14881
+ var encoder = new TextEncoder();
14882
+ var decoder = new TextDecoder();
14883
+ var observabilityEventSchema = external_exports.object({ ord: external_exports.number().int().min(1), name: external_exports.string().min(1).max(MAX_APP_EVENT_NAME_BYTES), status: external_exports.enum(APP_EVENT_STATUSES) }).strict();
14884
+ var savedErrorSchema = external_exports.object({
14885
+ ord: external_exports.number().int().min(1),
14886
+ category: external_exports.enum(APP_ERROR_CATEGORIES),
14887
+ action: external_exports.string().min(1).max(MAX_APP_ACTION_NAME_BYTES),
14888
+ description: external_exports.string().min(1).max(MAX_APP_ERROR_DESCRIPTION_BYTES),
14889
+ payload: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional(),
14890
+ error: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional()
14891
+ }).strict();
14892
+ var appObservabilityRecordSchema = external_exports.object({
14893
+ v: external_exports.literal(APP_OBSERVABILITY_VERSION),
14894
+ truncated: external_exports.boolean().optional(),
14895
+ events: external_exports.array(observabilityEventSchema).max(MAX_APP_OBSERVABILITY_EVENTS),
14896
+ errors: external_exports.array(savedErrorSchema).max(MAX_APP_SAVED_ERRORS)
14897
+ }).strict();
14898
+ var utf8Length = (value) => {
14899
+ return encoder.encode(value).length;
14900
+ };
14901
+ var truncateUtf8 = (value, maxBytes) => {
14902
+ if (utf8Length(value) <= maxBytes) {
14903
+ return value;
14904
+ }
14905
+ let kept = "";
14906
+ let used = 0;
14907
+ for (const character of value) {
14908
+ const size = utf8Length(character);
14909
+ if (used + size > maxBytes) {
14910
+ break;
14911
+ }
14912
+ kept += character;
14913
+ used += size;
14914
+ }
14915
+ return kept;
14916
+ };
14917
+ var serializeAppObservability = (record2) => {
14918
+ const bytes = encoder.encode(JSON.stringify(record2));
14919
+ let binary = "";
14920
+ for (const byte of bytes) {
14921
+ binary += String.fromCharCode(byte);
14922
+ }
14923
+ return btoa(binary);
14924
+ };
14925
+
14764
14926
  // ../../../libs/app-envelope/src/ui.ts
14765
14927
  var UI_CONTEXT_HEADER = "x-qinto-ui-context";
14766
14928
  var MAX_UI_BODY_BYTES = 1024 * 1024;
@@ -14806,10 +14968,10 @@ var formatSpansRecord = (record2) => {
14806
14968
  };
14807
14969
 
14808
14970
  // ../../../libs/tail-trace/src/app-spans.ts
14809
- var encoder = new TextEncoder();
14810
- var decoder = new TextDecoder();
14971
+ var encoder2 = new TextEncoder();
14972
+ var decoder2 = new TextDecoder();
14811
14973
  var serializeAppSpans = (record2) => {
14812
- const bytes = encoder.encode(JSON.stringify(record2));
14974
+ const bytes = encoder2.encode(JSON.stringify(record2));
14813
14975
  let binary = "";
14814
14976
  for (const byte of bytes) {
14815
14977
  binary += String.fromCharCode(byte);
@@ -14818,12 +14980,12 @@ var serializeAppSpans = (record2) => {
14818
14980
  };
14819
14981
 
14820
14982
  // ../../../libs/tail-trace/src/budget.ts
14821
- var encoder2 = new TextEncoder();
14983
+ var encoder3 = new TextEncoder();
14822
14984
  var byteLength = (value) => {
14823
- return encoder2.encode(JSON.stringify(value)).length;
14985
+ return encoder3.encode(JSON.stringify(value)).length;
14824
14986
  };
14825
14987
  var fitsBudget = (text, max) => {
14826
- return text.length * 3 <= max || encoder2.encode(text).length <= max;
14988
+ return text.length * 3 <= max || encoder3.encode(text).length <= max;
14827
14989
  };
14828
14990
  var MIN_KEEP = 64;
14829
14991
  var halveLongestString = (holders) => {
@@ -15407,6 +15569,162 @@ var buildAppsClient = (env, envelope) => {
15407
15569
  };
15408
15570
  };
15409
15571
 
15572
+ // src/oauth.ts
15573
+ var EXPIRY_FLOOR_MS = 1e3;
15574
+ var read = (env, id, ord, now) => {
15575
+ const raw = env[appOauthBindingName(id, ord)];
15576
+ const parsed = parseAppOauthBinding(typeof raw === "string" ? raw : void 0);
15577
+ if (parsed === null) {
15578
+ return null;
15579
+ }
15580
+ const expired = parsed.e !== null && parsed.e - now < EXPIRY_FLOOR_MS;
15581
+ return {
15582
+ id,
15583
+ status: parsed.st === "connected" && !expired ? "connected" : "reconnect_required",
15584
+ expiresAt: parsed.e
15585
+ };
15586
+ };
15587
+ var buildOauth = (env, ord, now = Date.now) => {
15588
+ return {
15589
+ token: (id) => {
15590
+ const raw = env[appOauthBindingName(id, ord)];
15591
+ const parsed = parseAppOauthBinding(typeof raw === "string" ? raw : void 0);
15592
+ if (parsed === null) {
15593
+ throw new Error(`not_connected: this install has no ${id} connection`);
15594
+ }
15595
+ const state = read(env, id, ord, now());
15596
+ if (parsed.t === void 0 || state?.status !== "connected") {
15597
+ throw new Error(`reconnect_required: the ${id} connection has ended and a site manager must reconnect it`);
15598
+ }
15599
+ return parsed.t;
15600
+ },
15601
+ connection: (id) => {
15602
+ return read(env, id, ord, now());
15603
+ }
15604
+ };
15605
+ };
15606
+ var oauthTokenValues = (env) => {
15607
+ const values = /* @__PURE__ */ new Set();
15608
+ for (const [name, raw] of Object.entries(env)) {
15609
+ if (!name.startsWith("OAUTH_") || typeof raw !== "string") {
15610
+ continue;
15611
+ }
15612
+ const token = parseAppOauthBinding(raw)?.t;
15613
+ if (token !== void 0 && token !== "") {
15614
+ values.add(token);
15615
+ }
15616
+ }
15617
+ return [...values];
15618
+ };
15619
+ var buildUiOauth = (env, installs, now = Date.now) => {
15620
+ const ordinalOf = (installId) => {
15621
+ return installs.find((install) => install.id === installId)?.ord ?? null;
15622
+ };
15623
+ return {
15624
+ token: (id, options) => {
15625
+ const ord = ordinalOf(options.installId);
15626
+ if (ord === null) {
15627
+ throw new Error(`not_connected: ${options.installId} is not an install of this app`);
15628
+ }
15629
+ return buildOauth(env, ord, now).token(id);
15630
+ },
15631
+ connection: (id, options) => {
15632
+ const ord = ordinalOf(options.installId);
15633
+ return ord === null ? null : buildOauth(env, ord, now).connection(id);
15634
+ }
15635
+ };
15636
+ };
15637
+
15638
+ // src/observability-collector.ts
15639
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
15640
+ var ENVELOPE_OVERHEAD = 64;
15641
+ var collectors = new AsyncLocalStorage2();
15642
+ var serialized = (value) => {
15643
+ if (value instanceof Error) {
15644
+ const stack = value.stack;
15645
+ return JSON.stringify({ name: value.name, message: value.message, ...stack === void 0 ? {} : { stack } });
15646
+ }
15647
+ let text;
15648
+ try {
15649
+ text = JSON.stringify(value);
15650
+ } catch {
15651
+ return String(value);
15652
+ }
15653
+ return typeof text === "string" ? text : String(value);
15654
+ };
15655
+ var field = (value, redact, maxBytes) => {
15656
+ return truncateUtf8(redact(value), maxBytes);
15657
+ };
15658
+ var optionalField = (value, redact) => {
15659
+ if (value === void 0) {
15660
+ return void 0;
15661
+ }
15662
+ return field(serialized(value), redact, MAX_APP_ERROR_PAYLOAD_BYTES);
15663
+ };
15664
+ var createObservabilityCollector = (redact) => {
15665
+ const events = [];
15666
+ const errors = [];
15667
+ let used = ENVELOPE_OVERHEAD;
15668
+ let truncated = false;
15669
+ const size = (entry) => {
15670
+ return utf8Length(JSON.stringify(entry)) + 1;
15671
+ };
15672
+ return {
15673
+ addEvent: (ord, name, status) => {
15674
+ if (name === "" || !APP_EVENT_STATUSES.includes(status)) {
15675
+ truncated = true;
15676
+ return;
15677
+ }
15678
+ if (events.length >= MAX_APP_OBSERVABILITY_EVENTS) {
15679
+ truncated = true;
15680
+ return;
15681
+ }
15682
+ const entry = { ord, name: field(name, redact, MAX_APP_EVENT_NAME_BYTES), status };
15683
+ used += size(entry);
15684
+ events.push(entry);
15685
+ },
15686
+ addError: (ord, input) => {
15687
+ if (!APP_ERROR_CATEGORIES.includes(input.category) || input.action === "" || input.description === "") {
15688
+ truncated = true;
15689
+ return;
15690
+ }
15691
+ if (errors.length >= MAX_APP_SAVED_ERRORS) {
15692
+ truncated = true;
15693
+ return;
15694
+ }
15695
+ const payload = optionalField(input.payload, redact);
15696
+ const error51 = optionalField(input.error, redact);
15697
+ const entry = {
15698
+ ord,
15699
+ category: input.category,
15700
+ action: field(input.action, redact, MAX_APP_ACTION_NAME_BYTES),
15701
+ description: field(input.description, redact, MAX_APP_ERROR_DESCRIPTION_BYTES),
15702
+ ...payload === void 0 ? {} : { payload },
15703
+ ...error51 === void 0 ? {} : { error: error51 }
15704
+ };
15705
+ const entrySize = size(entry);
15706
+ if (used + entrySize > APP_OBSERVABILITY_JSON_BUDGET) {
15707
+ truncated = true;
15708
+ return;
15709
+ }
15710
+ used += entrySize;
15711
+ errors.push(entry);
15712
+ },
15713
+ record: () => {
15714
+ if (events.length === 0 && errors.length === 0) {
15715
+ return null;
15716
+ }
15717
+ return { v: APP_OBSERVABILITY_VERSION, ...truncated ? { truncated } : {}, events, errors };
15718
+ }
15719
+ };
15720
+ };
15721
+ var runWithObservability = (collector, run) => {
15722
+ return collectors.run(collector, run);
15723
+ };
15724
+ var currentObservability = () => {
15725
+ return collectors.getStore();
15726
+ };
15727
+
15410
15728
  // src/observability.ts
15411
15729
  var exceptionAttrs = (error51) => {
15412
15730
  const attrs = {
@@ -15429,7 +15747,7 @@ var consoleText = (error51) => {
15429
15747
  }
15430
15748
  return redactUrlParams(error51 instanceof Error ? error51.stack ?? error51.message : String(error51));
15431
15749
  };
15432
- var buildObservability = () => {
15750
+ var buildObservability = (ord) => {
15433
15751
  return {
15434
15752
  log: (message, attrs) => {
15435
15753
  addSpanEvent(message, attrs === void 0 ? {} : { attrs });
@@ -15439,6 +15757,21 @@ var buildObservability = () => {
15439
15757
  addSpanEvent(message, { level: "error", attrs: { ...attrs, ...error51 === void 0 ? {} : exceptionAttrs(error51) } });
15440
15758
  console.error(redactUrlParams(message), consoleText(error51), redactAttrs(attrs ?? {}));
15441
15759
  },
15760
+ event: (name, status) => {
15761
+ addSpanEvent(name, { attrs: { "app.event.status": status } });
15762
+ if (ord !== null) {
15763
+ currentObservability()?.addEvent(ord, name, status);
15764
+ }
15765
+ },
15766
+ saveError: (input) => {
15767
+ addSpanEvent(input.description, {
15768
+ level: "error",
15769
+ attrs: { "app.error.category": input.category, "app.error.action": input.action }
15770
+ });
15771
+ if (ord !== null) {
15772
+ currentObservability()?.addError(ord, input);
15773
+ }
15774
+ },
15442
15775
  span: ((...args) => {
15443
15776
  const [name] = args;
15444
15777
  const { attrs, run } = spanParts(args);
@@ -15515,6 +15848,9 @@ var secretValues = (env) => {
15515
15848
  values.add(value);
15516
15849
  }
15517
15850
  }
15851
+ for (const value of oauthTokenValues(env)) {
15852
+ values.add(value);
15853
+ }
15518
15854
  return [...values];
15519
15855
  };
15520
15856
  var projectInstallValues = (maps, kinds, ord) => {
@@ -15591,13 +15927,13 @@ var readOnce = (env, subject) => {
15591
15927
  };
15592
15928
  };
15593
15929
  var fetchedContext = (env, subject, collector) => {
15594
- const read = readOnce(env, () => {
15930
+ const read2 = readOnce(env, () => {
15595
15931
  return subject("context.load");
15596
15932
  });
15597
15933
  return {
15598
15934
  private: {
15599
15935
  load: async () => {
15600
- return (await read()).private;
15936
+ return (await read2()).private;
15601
15937
  },
15602
15938
  set: (patch) => {
15603
15939
  subject("context.set");
@@ -15606,7 +15942,7 @@ var fetchedContext = (env, subject, collector) => {
15606
15942
  },
15607
15943
  shared: {
15608
15944
  load: async () => {
15609
- return (await read()).shared;
15945
+ return (await read2()).shared;
15610
15946
  },
15611
15947
  set: (patch) => {
15612
15948
  subject("context.set");
@@ -15616,13 +15952,13 @@ var fetchedContext = (env, subject, collector) => {
15616
15952
  };
15617
15953
  };
15618
15954
  var asyncContext = (env, subject) => {
15619
- const read = readOnce(env, () => {
15955
+ const read2 = readOnce(env, () => {
15620
15956
  return subject("context.load");
15621
15957
  });
15622
15958
  return {
15623
15959
  private: {
15624
15960
  load: async () => {
15625
- return (await read()).private;
15961
+ return (await read2()).private;
15626
15962
  },
15627
15963
  set: async (patch) => {
15628
15964
  assertValidPatch(patch);
@@ -15634,7 +15970,7 @@ var asyncContext = (env, subject) => {
15634
15970
  },
15635
15971
  shared: {
15636
15972
  load: async () => {
15637
- return (await read()).shared;
15973
+ return (await read2()).shared;
15638
15974
  },
15639
15975
  set: async (patch) => {
15640
15976
  assertValidPatch(patch);
@@ -15694,12 +16030,13 @@ var buildWorkflowContext = (env, stamp) => {
15694
16030
  env,
15695
16031
  install,
15696
16032
  startWorkflow: buildStartWorkflow(stamp),
16033
+ oauth: buildOauth(env, stamp.ord),
15697
16034
  variables,
15698
16035
  secrets,
15699
16036
  appVariables,
15700
16037
  appSecrets,
15701
16038
  userId: stamp.anonId,
15702
- observability: buildObservability(),
16039
+ observability: buildObservability(stamp.ord),
15703
16040
  context: asyncContext(env, (what) => {
15704
16041
  return requireSubject(stamp.anonId, what);
15705
16042
  }),
@@ -15776,6 +16113,7 @@ var buildContext = ({
15776
16113
  secrets,
15777
16114
  appVariables: appScope.appVariables,
15778
16115
  appSecrets: appScope.appSecrets,
16116
+ oauth: buildOauth(env, install.ord),
15779
16117
  user: envelope.user ?? null,
15780
16118
  consent: envelope.consent,
15781
16119
  geo: envelope.geo,
@@ -15830,7 +16168,7 @@ var buildContext = ({
15830
16168
  }
15831
16169
  }
15832
16170
  },
15833
- observability: buildObservability(),
16171
+ observability: buildObservability(install.ord),
15834
16172
  apps: buildAppsClient(env, envelope)
15835
16173
  };
15836
16174
  };
@@ -15902,7 +16240,8 @@ var buildCatalogQuery = ({ env, ticket, installId }) => {
15902
16240
  // src/schedule.ts
15903
16241
  var dispatchSchedule = async (envelope, bundle, env) => {
15904
16242
  const schedule = envelope.schedule;
15905
- const handler = bundle.schedules?.[schedule.id];
16243
+ const schedules = bundle.schedules;
16244
+ const handler = schedules !== void 0 && Object.hasOwn(schedules, schedule.id) ? schedules[schedule.id] : void 0;
15906
16245
  if (typeof handler !== "function") {
15907
16246
  return json2(200, {
15908
16247
  v: CONTROL_ENVELOPE_VERSION,
@@ -15927,11 +16266,12 @@ var dispatchSchedule = async (envelope, bundle, env) => {
15927
16266
  installLabel: install.label,
15928
16267
  anonId: null
15929
16268
  }),
16269
+ oauth: buildOauth(env, install.ord),
15930
16270
  variables,
15931
16271
  secrets,
15932
16272
  appVariables: appScope.appVariables,
15933
16273
  appSecrets: appScope.appSecrets,
15934
- observability: buildObservability(),
16274
+ observability: buildObservability(install.ord),
15935
16275
  apps: buildAppsClient(env, { depth: 0 }),
15936
16276
  query: buildCatalogQuery({ env, ticket: install.catalogQueryTicket, installId: install.id }),
15937
16277
  push: async (userId, message) => {
@@ -15963,6 +16303,141 @@ var dispatchSchedule = async (envelope, bundle, env) => {
15963
16303
  return json2(200, { v: CONTROL_ENVELOPE_VERSION, ok: true });
15964
16304
  };
15965
16305
 
16306
+ // src/tool.ts
16307
+ var failure = (message) => {
16308
+ return json2(200, { v: CONTROL_ENVELOPE_VERSION, ok: false, error: { message } });
16309
+ };
16310
+ var isFailure = (value) => {
16311
+ return value !== null && typeof value === "object" && value.kind === "tool_error" && typeof value.message === "string";
16312
+ };
16313
+ var isResult = (value) => {
16314
+ return value !== null && typeof value === "object" && typeof value.content === "string";
16315
+ };
16316
+ var isStructuredData = (data) => {
16317
+ return data === void 0 || data !== null && typeof data === "object" && !Array.isArray(data);
16318
+ };
16319
+ var isPlainContainer = (value) => {
16320
+ const prototype = Object.getPrototypeOf(value);
16321
+ return Array.isArray(value) || prototype === Object.prototype || prototype === null;
16322
+ };
16323
+ var jsonScalarVerdict = (value) => {
16324
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
16325
+ return "ok";
16326
+ }
16327
+ if (typeof value === "number") {
16328
+ return Number.isFinite(value) ? "ok" : "invalid";
16329
+ }
16330
+ if (value === void 0 || typeof value !== "object" || !isPlainContainer(value)) {
16331
+ return "invalid";
16332
+ }
16333
+ return "container";
16334
+ };
16335
+ var isJsonValue = (root) => {
16336
+ const stack = [root];
16337
+ const seen = /* @__PURE__ */ new Set();
16338
+ while (stack.length > 0) {
16339
+ const value = stack.pop();
16340
+ const verdict = jsonScalarVerdict(value);
16341
+ if (verdict === "invalid") {
16342
+ return false;
16343
+ }
16344
+ if (verdict === "ok" || seen.has(value)) {
16345
+ continue;
16346
+ }
16347
+ seen.add(value);
16348
+ if (Array.isArray(value) && Object.keys(value).length !== value.length) {
16349
+ return false;
16350
+ }
16351
+ stack.push(...Object.values(value));
16352
+ }
16353
+ return true;
16354
+ };
16355
+ var truncate = (message) => {
16356
+ return message.length <= MAX_TOOL_ERROR_MESSAGE_LENGTH ? message : message.slice(0, MAX_TOOL_ERROR_MESSAGE_LENGTH);
16357
+ };
16358
+ var checkResult = (result) => {
16359
+ if (!isStructuredData(result.data)) {
16360
+ return "not_structured";
16361
+ }
16362
+ if (result.data !== void 0 && !isJsonValue(result.data)) {
16363
+ return "not_serializable";
16364
+ }
16365
+ let encoded;
16366
+ try {
16367
+ encoded = new TextEncoder().encode(JSON.stringify({ content: result.content, data: result.data ?? null }));
16368
+ } catch {
16369
+ return "not_serializable";
16370
+ }
16371
+ if (encoded.length > MAX_TOOL_RESULT_BYTES || !toolDataWithinLimits(result.data)) {
16372
+ return "too_large";
16373
+ }
16374
+ return "ok";
16375
+ };
16376
+ var buildContext2 = (envelope, env) => {
16377
+ const install = envelope.install;
16378
+ const { variableKinds, appVariableKinds } = contextKindsOf(env);
16379
+ const maps = parseVariableMaps(env);
16380
+ const appScope = projectAppValues(env, appVariableKinds);
16381
+ const { variables, secrets } = projectInstallValues(maps, variableKinds, install.ord);
16382
+ return {
16383
+ env,
16384
+ site: envelope.site,
16385
+ install: { id: install.id, label: install.label },
16386
+ viewer: envelope.tool.viewer,
16387
+ callId: envelope.tool.callId,
16388
+ startWorkflow: buildStartWorkflow({ ord: install.ord, installId: install.id, installLabel: install.label, anonId: null }),
16389
+ oauth: buildOauth(env, install.ord),
16390
+ variables,
16391
+ secrets,
16392
+ appVariables: appScope.appVariables,
16393
+ appSecrets: appScope.appSecrets,
16394
+ observability: buildObservability(install.ord),
16395
+ apps: buildAppsClient(env, { depth: 0 }),
16396
+ fail: (message) => {
16397
+ return { kind: "tool_error", message: truncate(message) };
16398
+ }
16399
+ };
16400
+ };
16401
+ var dispatchTool = async (envelope, bundle, env) => {
16402
+ const name = envelope.tool.name;
16403
+ const tools = bundle.tools;
16404
+ const handler = tools !== void 0 && Object.hasOwn(tools, name) ? tools[name] : void 0;
16405
+ if (typeof handler !== "function") {
16406
+ return failure(`missing handler: tools.${name}`);
16407
+ }
16408
+ setSpanAttributes({ "app.tool": name, "app.tool_call_id": envelope.tool.callId });
16409
+ let value;
16410
+ try {
16411
+ value = await installSpan(envelope.install, () => {
16412
+ return handler(envelope.tool.arguments, buildContext2(envelope, env));
16413
+ });
16414
+ } catch (error51) {
16415
+ console.warn(`tool ${name} failed for install ${envelope.install.id}: ${failureMessage(error51)}`);
16416
+ return failure(`tool ${name} failed`);
16417
+ }
16418
+ if (isFailure(value)) {
16419
+ return json2(200, { v: CONTROL_ENVELOPE_VERSION, ok: true, tool: { content: truncate(value.message), failed: true } });
16420
+ }
16421
+ if (!isResult(value)) {
16422
+ return failure(`tool ${name} returned no result`);
16423
+ }
16424
+ const verdict = checkResult(value);
16425
+ if (verdict === "not_structured") {
16426
+ return failure(`tool ${name} returned data that is not a JSON object`);
16427
+ }
16428
+ if (verdict === "not_serializable") {
16429
+ return failure(`tool ${name} returned data that cannot be serialized`);
16430
+ }
16431
+ if (verdict === "too_large") {
16432
+ return failure(`tool ${name} returned a result over the size cap`);
16433
+ }
16434
+ return json2(200, {
16435
+ v: CONTROL_ENVELOPE_VERSION,
16436
+ ok: true,
16437
+ tool: { content: value.content, ...value.data === void 0 ? {} : { data: value.data }, failed: false }
16438
+ });
16439
+ };
16440
+
15966
16441
  // src/control.ts
15967
16442
  var controlError = (status, message) => {
15968
16443
  return json2(status, { v: CONTROL_ENVELOPE_VERSION, ok: false, error: { message } });
@@ -15993,7 +16468,7 @@ var lifecycleContext = (env, envelope) => {
15993
16468
  installLabel: install.label,
15994
16469
  anonId: null
15995
16470
  }),
15996
- observability: buildObservability()
16471
+ observability: buildObservability(install.ord)
15997
16472
  };
15998
16473
  };
15999
16474
  var migrationContext = (env, envelope) => {
@@ -16003,7 +16478,9 @@ var migrationContext = (env, envelope) => {
16003
16478
  site: envelope.site,
16004
16479
  appVariables: appScope.appVariables,
16005
16480
  appSecrets: appScope.appSecrets,
16006
- observability: buildObservability()
16481
+ // A migration runs once for the site, not once per install, so it has no install to attribute
16482
+ // an event or a saved error to.
16483
+ observability: buildObservability(null)
16007
16484
  };
16008
16485
  };
16009
16486
  var runStatementMigration = async (env, migration) => {
@@ -16100,6 +16577,9 @@ var dispatchControl = async ({ request, env, versions }) => {
16100
16577
  case "schedule": {
16101
16578
  return dispatchSchedule(envelope, bundle, env);
16102
16579
  }
16580
+ case "tool": {
16581
+ return dispatchTool(envelope, bundle, env);
16582
+ }
16103
16583
  }
16104
16584
  };
16105
16585
 
@@ -16132,19 +16612,22 @@ var handlerRequest = (request, context, body) => {
16132
16612
  body: body.byteLength === 0 ? void 0 : body
16133
16613
  });
16134
16614
  };
16135
- var buildContext2 = ({ env, maps, kinds, appScope, install }) => {
16615
+ var buildContext3 = ({ env, maps, kinds, appScope, install, site, params }) => {
16136
16616
  const { variables, secrets } = projectInstallValues(maps, kinds, install.ord);
16137
16617
  return {
16138
16618
  env,
16619
+ site,
16620
+ params,
16139
16621
  install: { id: install.id, label: install.label },
16140
16622
  // An endpoint has no visitor, so a workflow it starts cannot push. `push` below is the
16141
16623
  // exception: it names its subject rather than inheriting one.
16142
16624
  startWorkflow: buildStartWorkflow({ ord: install.ord, installId: install.id, installLabel: install.label, anonId: null }),
16625
+ oauth: buildOauth(env, install.ord),
16143
16626
  variables,
16144
16627
  secrets,
16145
16628
  appVariables: appScope.appVariables,
16146
16629
  appSecrets: appScope.appSecrets,
16147
- observability: buildObservability(),
16630
+ observability: buildObservability(install.ord),
16148
16631
  apps: buildAppsClient(env, { depth: 0 }),
16149
16632
  push: async (userId, message) => {
16150
16633
  await brokerPush(env, {
@@ -16207,7 +16690,8 @@ var dispatchEndpoint = async ({ request, env, versions }) => {
16207
16690
  return Response.json({ error: "unknown version" }, { status: 404 });
16208
16691
  }
16209
16692
  const bundle = versions[context.version];
16210
- const candidate = bundle?.endpoints?.[context.path];
16693
+ const endpoints = bundle?.endpoints;
16694
+ const candidate = endpoints !== void 0 && Object.hasOwn(endpoints, context.route) ? endpoints[context.route] : void 0;
16211
16695
  if (typeof candidate !== "function") {
16212
16696
  return Response.json({ error: "not_found" }, { status: 404 });
16213
16697
  }
@@ -16218,7 +16702,10 @@ var dispatchEndpoint = async ({ request, env, versions }) => {
16218
16702
  const appScope = projectAppValues(env, appVariableKinds);
16219
16703
  const invoke = async (install) => {
16220
16704
  return installSpan(install, () => {
16221
- return handler(handlerRequest(request, context, body), buildContext2({ env, maps, kinds: variableKinds, appScope, install }));
16705
+ return handler(
16706
+ handlerRequest(request, context, body),
16707
+ buildContext3({ env, maps, kinds: variableKinds, appScope, install, site: context.site, params: context.params })
16708
+ );
16222
16709
  });
16223
16710
  };
16224
16711
  const installs = [...context.installs].sort((left, right) => {
@@ -16241,29 +16728,38 @@ var appSlug = (context) => {
16241
16728
  return "app";
16242
16729
  }
16243
16730
  };
16244
- var withSpans = (response, record2) => {
16731
+ var withSpans = (response, record2, collector) => {
16245
16732
  const headers = new Headers(response.headers);
16246
16733
  headers.set(APP_SPANS_HEADER, serializeAppSpans(record2));
16734
+ headers.delete(APP_OBSERVABILITY_HEADER);
16735
+ const observability = collector.record();
16736
+ if (observability !== null) {
16737
+ headers.set(APP_OBSERVABILITY_HEADER, serializeAppObservability(observability));
16738
+ }
16247
16739
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
16248
16740
  };
16249
16741
  var runAppInvocation = async (input, handle) => {
16250
16742
  let record2 = { spans: [] };
16743
+ const redact = spanRedactor(input.secrets);
16744
+ const collector = createObservabilityCollector(redact);
16251
16745
  try {
16252
16746
  const response = await runInvocation(
16253
16747
  {
16254
16748
  name: appSlug(input.context),
16255
16749
  trace: parseTraceContext(input.request.headers.get(TRACE_HEADER)),
16256
16750
  budget: APP_SPANS_JSON_BUDGET,
16257
- redact: spanRedactor(input.secrets),
16751
+ redact,
16258
16752
  emit: (emitted) => {
16259
16753
  record2 = emitted;
16260
16754
  }
16261
16755
  },
16262
- handle
16756
+ () => {
16757
+ return runWithObservability(collector, handle);
16758
+ }
16263
16759
  );
16264
- return withSpans(response, record2);
16760
+ return withSpans(response, record2, collector);
16265
16761
  } catch {
16266
- return withSpans(input.failure(), record2);
16762
+ return withSpans(input.failure(), record2, collector);
16267
16763
  }
16268
16764
  };
16269
16765
 
@@ -16289,21 +16785,21 @@ var capturedBody = async (open) => {
16289
16785
  return void 0;
16290
16786
  }
16291
16787
  const reader = body.getReader();
16292
- const decoder2 = new TextDecoder();
16788
+ const decoder3 = new TextDecoder();
16293
16789
  const expired = new Promise((resolve) => {
16294
16790
  timer = setTimeout(() => resolve(null), READ_BUDGET_MS);
16295
16791
  });
16296
16792
  let text = "";
16297
16793
  for (; ; ) {
16298
- const read = await Promise.race([reader.read(), expired]);
16299
- if (read === null) {
16794
+ const read2 = await Promise.race([reader.read(), expired]);
16795
+ if (read2 === null) {
16300
16796
  abandon(reader);
16301
16797
  return INCOMPLETE;
16302
16798
  }
16303
- if (read.done) {
16799
+ if (read2.done) {
16304
16800
  break;
16305
16801
  }
16306
- text += decoder2.decode(read.value, { stream: true });
16802
+ text += decoder3.decode(read2.value, { stream: true });
16307
16803
  if (text.length > MAX_BODY_CHARS) {
16308
16804
  abandon(reader);
16309
16805
  return TOO_LARGE;
@@ -16415,6 +16911,9 @@ var dispatchUi = async ({ request, env, versions }) => {
16415
16911
  env,
16416
16912
  install: { id: context.install.id, label: context.install.label },
16417
16913
  installs: projectAllInstallValues(maps, variableKinds, context.installs),
16914
+ // The install is named rather than inherited: one panel serves every install, and reading the
16915
+ // subject install's token while showing another account's UI would be silently wrong.
16916
+ oauth: buildUiOauth(env, context.installs),
16418
16917
  settings: buildSettings({ env, ticket: context.writeTicket }),
16419
16918
  // The panel's viewer is a portal user, not a visitor, so a workflow it starts carries no subject.
16420
16919
  startWorkflow: buildUiStartWorkflow(
@@ -16426,7 +16925,7 @@ var dispatchUi = async ({ request, env, versions }) => {
16426
16925
  appVariables,
16427
16926
  appSecrets,
16428
16927
  viewer: context.viewer,
16429
- observability: buildObservability(),
16928
+ observability: buildObservability(context.install.ord),
16430
16929
  apps: buildAppsClient(env, { depth: 0 })
16431
16930
  };
16432
16931
  let response;
@@ -16491,7 +16990,7 @@ var runInstalls = async (envelope, env, invoke) => {
16491
16990
  const { variableKinds: kinds, appVariableKinds } = contextKindsOf(env);
16492
16991
  const appScope = projectAppValues(env, appVariableKinds);
16493
16992
  const groups = [];
16494
- let failure;
16993
+ let failure2;
16495
16994
  for (const install of envelope.installs) {
16496
16995
  const emits = [];
16497
16996
  const pushes = [];
@@ -16505,7 +17004,7 @@ var runInstalls = async (envelope, env, invoke) => {
16505
17004
  } catch (error51) {
16506
17005
  const message = failureMessage(error51);
16507
17006
  console.warn(`app hook failed for install ${install.id}: ${message}`);
16508
- failure ??= message;
17007
+ failure2 ??= message;
16509
17008
  continue;
16510
17009
  }
16511
17010
  const contextWrites = context.writes();
@@ -16517,8 +17016,8 @@ var runInstalls = async (envelope, env, invoke) => {
16517
17016
  if (groups.length > 0) {
16518
17017
  body.effects = groups;
16519
17018
  }
16520
- if (failure !== void 0) {
16521
- body.error = { message: failure };
17019
+ if (failure2 !== void 0) {
17020
+ body.error = { message: failure2 };
16522
17021
  }
16523
17022
  return json2(200, body);
16524
17023
  };