@agent-surface/core 0.4.1 → 0.6.0

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.
package/dist/index.d.ts CHANGED
@@ -171,6 +171,14 @@ declare function assignWireNames(entries: readonly WireNameEntry[]): WireNameAss
171
171
  * names among them (`AS-WIRE-007`: consult `toolset.wireNameMap()` instead).
172
172
  * Returning a plausible-but-wrong canonical id would take the audit identity
173
173
  * with it, so this refuses anything it cannot re-encode byte-identically.
174
+ *
175
+ * Refusal is decided by what a name *is*, never by a substring it happens to
176
+ * contain. `view:at.a.a` encodes to `view_at__a__a`, where `_at_` is the plane
177
+ * separator meeting a segment named "at" — screening for the marker text cost
178
+ * every id with an `at` or `0` segment its own faithful encoding (`AS-ID-004`).
179
+ * Marker-bearing names are still refused, by the two checks that can tell:
180
+ * every underscore run must be exactly two (one "."), and the id must re-encode
181
+ * byte-identically.
174
182
  */
175
183
  declare function decodeWireName(name: string): string | undefined;
176
184
 
@@ -749,17 +757,10 @@ interface AgentProcedureDescriptor {
749
757
  procedureId: string;
750
758
  /**
751
759
  * The manifest description. Stable across snapshots — the contextual
752
- * `describe()` output is `contextualNote`, not part of this string, unless
753
- * the registry was created with `snapshotMergesContextualNote: true`
754
- * (the 0.2 default, removed in a later minor — D28).
760
+ * `describe()` output is `contextualNote` and is never folded in here (D28).
755
761
  */
756
762
  description: string;
757
- /**
758
- * Volatile: this snapshot's contextual `describe()` output, if any. Always
759
- * populated, in both merge modes, so a host can migrate before the default
760
- * moves. Use {@link stableDescriptionOf} to recover the note-free
761
- * description without parsing.
762
- */
763
+ /** Volatile: this snapshot's contextual `describe()` output, if any. */
763
764
  contextualNote?: string;
764
765
  /** Agent-facing (reduced) input schema per binding rule 1 (docs/05). */
765
766
  inputSchema: JsonSchema;
@@ -783,15 +784,6 @@ interface AgentProcedureDescriptor {
783
784
  meta?: Record<string, JsonValue>;
784
785
  }
785
786
  type AgentCapabilityDescriptorUnion = AgentObservationDescriptor | AgentActionDescriptor | AgentProcedureDescriptor;
786
- /**
787
- * The note-free description of a descriptor, whichever way the registry was
788
- * configured to compose it (D28). The merge rule is `${description} ${note}`,
789
- * so the split is exact — no host ever has to parse a prefix it did not write.
790
- */
791
- declare function stableDescriptionOf(descriptor: {
792
- description: string;
793
- contextualNote?: string;
794
- }): string;
795
787
 
796
788
  interface RegistrationCandidate {
797
789
  definition: AgentComponentDefinition;
@@ -815,15 +807,6 @@ interface RegistryOptions {
815
807
  /** Route descriptor for snapshots (host wires its router here). */
816
808
  route?: () => AgentRouteInfo | undefined;
817
809
  limits?: Partial<AgentSurfaceLimits>;
818
- /**
819
- * D28 compatibility flag. `true` (default through 0.4; flips in 0.5) folds a procedure
820
- * reference's contextual `describe()` output into
821
- * `AgentProcedureDescriptor.description`, as 0.1 did. `false` keeps the two
822
- * apart, so `description` is stable across snapshots and the live text is
823
- * read from `contextualNote`. Populated either way; the default flips in a
824
- * later minor and the flag is then removed.
825
- */
826
- snapshotMergesContextualNote?: boolean;
827
810
  /** Injectable clock (docs/08 determinism); default Date.now. */
828
811
  now?: () => number;
829
812
  }
@@ -897,23 +880,14 @@ interface AgentToolsetOptions {
897
880
  maxComponents?: number;
898
881
  maxBytes?: number;
899
882
  };
900
- /**
901
- * D28 compatibility flag. `true` (default through 0.4; flips in 0.5) composes
902
- * availability and the contextual note into `description`, as 0.1 did.
903
- * `false` keeps `description` free of live state, so the provider tool block
904
- * is byte-stable across steps and prompt-prefix caching survives; the host
905
- * renders `AgentTool.state` outside the tool definitions (docs/09
906
- * §rendering-capability-state). `state` is populated either way.
907
- */
908
- descriptionIncludesState?: boolean;
909
883
  }
910
884
  interface AgentTool {
911
885
  /** Wire-safe name (docs/09 §wire-names), ≤ 64 chars, unique in this catalog. */
912
886
  name: string;
913
887
  /**
914
888
  * Plane + effect + confirmation prefix, then the authored description.
915
- * With `descriptionIncludesState: false` this contains NO live state it is
916
- * safe in a provider tool block with prompt-prefix caching across steps.
889
+ * Contains NO live state (D28), so it is safe in a provider tool block with
890
+ * prompt-prefix caching across steps.
917
891
  */
918
892
  description: string;
919
893
  inputSchema: JsonSchema;
@@ -950,4 +924,4 @@ declare function createAgentToolset(registry: AgentSurfaceRegistry, options: Age
950
924
  /** Deep equality over JsonValue (order-sensitive for arrays, docs/06 rule 2). */
951
925
  declare function jsonDeepEqual(a: JsonValue | undefined, b: JsonValue | undefined): boolean;
952
926
 
953
- export { AGENT_CAPABILITY_ERROR_CODES, type AgentActionContext, type AgentActionDefinition, type AgentActionDescriptor, type AgentAuthorizationContext, type AgentCapabilityDescriptorUnion, type AgentCapabilityErrorCode, type AgentCapabilityErrorPayload, type AgentComponentDefinition, type AgentComponentDescriptor, type AgentConcurrency, type AgentConsumer, type AgentEffect, type AgentEnvironment, type AgentErrorRetry, type AgentInvocation, type AgentInvocationPolicyContext, type AgentInvocationResult, type AgentObservationDefinition, type AgentObservationDescriptor, type AgentPlane, type AgentPolicy, type AgentPolicyContext, type AgentProcedureBinding, type AgentProcedureBindingRuntimeConfig, type AgentProcedureDescriptor, type AgentProcedureEffect, type AgentProcedureExecutor, type AgentProcedureRefDescriptor, type AgentReadContext, type AgentRegistrationHandle, type AgentRouteInfo, type AgentSchema, AgentSchemaError, type AgentSchemaIssue, AgentSurfaceDefinitionError, type AgentSurfaceDefinitionErrorCode, AgentSurfaceError, type AgentSurfaceEvent, type AgentSurfaceLimits, type AgentSurfaceRegistry, type AgentSurfaceSnapshot, type AgentTool, type AgentToolset, type AgentToolsetOptions, type AuditEvent, type AuditSink, CONFIRMATION_ESCALATION, type ConfirmationController, type ConfirmationEscalation, DEFAULT_LIMITS, type DiscoveryDecision, type InvokeOptions, type JsonSchema, type JsonValue, MAX_ID_LENGTH, MAX_WIRE_NAME_LENGTH, type ParsedCapabilityId, type PendingConfirmation, type PreconditionFailure, type ProcedureCallInfo, type RegistrationCandidate, type RegistryOptions, type SnapshotContext, type StandardSchemaV1, type Unsubscribe, type WireNameAssignment, type WireNameEntry, action, assignWireNames, audit, authenticated, composeInvokeChain, consoleAuditSink, createAgentSurfaceRegistry, createAgentToolset, decodeWireName, defineAgentComponent, emptyObjectSchema, encodeWireName, encodeWireNameForInstance, environment, evaluateDiscovery, formatDomainCapabilityId, formatViewCapabilityId, fromJsonSchema, fromStandardSchema, hasPermission, isAgentSurfaceError, isValidCapabilityName, isValidComponentType, isValidInstanceId, jsonDeepEqual, memoryAuditSink, observation, parseCapabilityId, rateLimit, requireConfirmation, stableDescriptionOf, tenantBoundary, validateComponentDefinition, validateJsonSchemaDocument, validateValueAgainstSchema };
927
+ export { AGENT_CAPABILITY_ERROR_CODES, type AgentActionContext, type AgentActionDefinition, type AgentActionDescriptor, type AgentAuthorizationContext, type AgentCapabilityDescriptorUnion, type AgentCapabilityErrorCode, type AgentCapabilityErrorPayload, type AgentComponentDefinition, type AgentComponentDescriptor, type AgentConcurrency, type AgentConsumer, type AgentEffect, type AgentEnvironment, type AgentErrorRetry, type AgentInvocation, type AgentInvocationPolicyContext, type AgentInvocationResult, type AgentObservationDefinition, type AgentObservationDescriptor, type AgentPlane, type AgentPolicy, type AgentPolicyContext, type AgentProcedureBinding, type AgentProcedureBindingRuntimeConfig, type AgentProcedureDescriptor, type AgentProcedureEffect, type AgentProcedureExecutor, type AgentProcedureRefDescriptor, type AgentReadContext, type AgentRegistrationHandle, type AgentRouteInfo, type AgentSchema, AgentSchemaError, type AgentSchemaIssue, AgentSurfaceDefinitionError, type AgentSurfaceDefinitionErrorCode, AgentSurfaceError, type AgentSurfaceEvent, type AgentSurfaceLimits, type AgentSurfaceRegistry, type AgentSurfaceSnapshot, type AgentTool, type AgentToolset, type AgentToolsetOptions, type AuditEvent, type AuditSink, CONFIRMATION_ESCALATION, type ConfirmationController, type ConfirmationEscalation, DEFAULT_LIMITS, type DiscoveryDecision, type InvokeOptions, type JsonSchema, type JsonValue, MAX_ID_LENGTH, MAX_WIRE_NAME_LENGTH, type ParsedCapabilityId, type PendingConfirmation, type PreconditionFailure, type ProcedureCallInfo, type RegistrationCandidate, type RegistryOptions, type SnapshotContext, type StandardSchemaV1, type Unsubscribe, type WireNameAssignment, type WireNameEntry, action, assignWireNames, audit, authenticated, composeInvokeChain, consoleAuditSink, createAgentSurfaceRegistry, createAgentToolset, decodeWireName, defineAgentComponent, emptyObjectSchema, encodeWireName, encodeWireNameForInstance, environment, evaluateDiscovery, formatDomainCapabilityId, formatViewCapabilityId, fromJsonSchema, fromStandardSchema, hasPermission, isAgentSurfaceError, isValidCapabilityName, isValidComponentType, isValidInstanceId, jsonDeepEqual, memoryAuditSink, observation, parseCapabilityId, rateLimit, requireConfirmation, tenantBoundary, validateComponentDefinition, validateJsonSchemaDocument, validateValueAgainstSchema };
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ function formatDomainCapabilityId(path) {
82
82
  return `domain:${path}`;
83
83
  }
84
84
  function parseCapabilityId(id) {
85
- if (id.length > MAX_ID_LENGTH) return void 0;
85
+ if (typeof id !== "string" || id.length > MAX_ID_LENGTH) return void 0;
86
86
  if (id.startsWith("view:")) {
87
87
  const rest = id.slice("view:".length);
88
88
  const lastDot = rest.lastIndexOf(".");
@@ -125,7 +125,7 @@ function encodeWireName(id) {
125
125
  }
126
126
  function encodeWireNameForInstance(id, instanceId, level = 0) {
127
127
  const raw = rawWireName(id, instanceId);
128
- if (level === 0 && raw.length <= MAX_WIRE_NAME_LENGTH) return raw;
128
+ if (level === 0 && raw.length <= MAX_WIRE_NAME_LENGTH && !id.includes("_")) return raw;
129
129
  const hashLength = 7 + level * 2;
130
130
  const keep = MAX_WIRE_NAME_LENGTH - SHORTENED_MARKER.length - hashLength;
131
131
  const hash = hash36(`${id}#${instanceId ?? ""}#${level}`, hashLength);
@@ -169,12 +169,15 @@ function assignWireNames(entries) {
169
169
  return { names, byName };
170
170
  }
171
171
  function decodeWireName(name) {
172
- if (name.includes(SHORTENED_MARKER) || name.includes(INSTANCE_MARKER)) return void 0;
173
172
  const planeEnd = name.indexOf("_");
174
173
  if (planeEnd <= 0) return void 0;
175
174
  const plane = name.slice(0, planeEnd);
176
175
  if (plane !== "view" && plane !== "domain") return void 0;
177
- const id = `${plane}:${name.slice(planeEnd + 1).replaceAll("__", ".")}`;
176
+ const rest = name.slice(planeEnd + 1);
177
+ if (/_{3,}/.test(rest)) return void 0;
178
+ const path = rest.replaceAll("__", ".");
179
+ if (path.split(".").some((segment) => segment === "")) return void 0;
180
+ const id = `${plane}:${path}`;
178
181
  if (id.includes("_") || !parseCapabilityId(id) || encodeWireName(id) !== name) return void 0;
179
182
  return id;
180
183
  }
@@ -1361,6 +1364,7 @@ var ConfirmationStore = class {
1361
1364
  };
1362
1365
 
1363
1366
  // src/internal.ts
1367
+ var DEV_WARN = /* @__PURE__ */ Symbol("agent-surface.dev-warn");
1364
1368
  var DevDefectError = class extends Error {
1365
1369
  constructor(message) {
1366
1370
  super(message);
@@ -2572,12 +2576,6 @@ function drainObservationQueues(internals) {
2572
2576
 
2573
2577
  // src/snapshot.ts
2574
2578
  var DEFAULT_CONSUMER2 = { id: "anonymous", kind: "embedded" };
2575
- function stableDescriptionOf(descriptor) {
2576
- const note = descriptor.contextualNote;
2577
- if (!note) return descriptor.description;
2578
- if (descriptor.description === note) return "";
2579
- return descriptor.description.endsWith(` ${note}`) ? descriptor.description.slice(0, -(note.length + 1)) : descriptor.description;
2580
- }
2581
2579
  function matchesScope(type, scope) {
2582
2580
  if (!scope || scope.length === 0) return true;
2583
2581
  return scope.some((prefix) => type === prefix || type.startsWith(`${prefix}.`));
@@ -2692,10 +2690,12 @@ function createSnapshot(internals, ctx) {
2692
2690
  } catch {
2693
2691
  }
2694
2692
  }
2695
- const description = contextualNote && internals.mergesContextualNote ? `${proc.baseDescription} ${contextualNote}`.trim() : proc.baseDescription;
2696
2693
  procedures.push({
2697
2694
  procedureId: proc.capabilityId,
2698
- description,
2695
+ // Never merged with `contextualNote` (D28): the manifest text is the
2696
+ // stable half, and folding volatile text in is what churned the
2697
+ // provider's cached prompt prefix.
2698
+ description: proc.baseDescription,
2699
2699
  ...contextualNote !== void 0 ? { contextualNote } : {},
2700
2700
  inputSchema: proc.reducedInputSchema,
2701
2701
  ...proc.outputJsonSchema ? { outputSchema: proc.outputJsonSchema } : {},
@@ -2763,7 +2763,6 @@ function createAgentSurfaceRegistry(options) {
2763
2763
  const internals = {
2764
2764
  environment: environment2,
2765
2765
  limits,
2766
- mergesContextualNote: options?.snapshotMergesContextualNote ?? true,
2767
2766
  surfaceId: `srf_${randomBase62(22)}`,
2768
2767
  version: 0,
2769
2768
  registrations: /* @__PURE__ */ new Map(),
@@ -3039,6 +3038,10 @@ function createAgentSurfaceRegistry(options) {
3039
3038
  dispatcher.clear();
3040
3039
  }
3041
3040
  };
3041
+ Object.defineProperty(registry, DEV_WARN, {
3042
+ value: (...args) => internals.devWarn(...args),
3043
+ enumerable: false
3044
+ });
3042
3045
  return registry;
3043
3046
  }
3044
3047
  function combineSinks(...sinks) {
@@ -3055,16 +3058,80 @@ var EMPTY_INPUT_SCHEMA = {
3055
3058
  properties: {},
3056
3059
  additionalProperties: false
3057
3060
  };
3061
+ var META_DISCOVER_SCHEMA = {
3062
+ type: "object",
3063
+ properties: {
3064
+ scope: {
3065
+ type: "array",
3066
+ items: { type: "string" },
3067
+ // No enum: valid tokens are live component types, and inlining them
3068
+ // would make this tool block churn on every mount — the churn
3069
+ // AS-META-005 and D28 exist to prevent.
3070
+ description: 'Component-type prefixes to narrow the result, e.g. ["devices.table"], taken from `components[].type` of an earlier call \u2014 omit on the first. Narrows only: prefixes outside this host\'s configured scope match nothing and come back in `scopeRejected`.'
3071
+ }
3072
+ },
3073
+ additionalProperties: false
3074
+ };
3075
+ var META_READ_SCHEMA = {
3076
+ type: "object",
3077
+ properties: {
3078
+ capabilityId: {
3079
+ type: "string",
3080
+ description: "Observation id, verbatim from `observations[].capabilityId` in a discover result."
3081
+ },
3082
+ instanceId: {
3083
+ type: "string",
3084
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3085
+ }
3086
+ },
3087
+ required: ["capabilityId"],
3088
+ additionalProperties: false
3089
+ };
3090
+ var META_ACT_SCHEMA = {
3091
+ type: "object",
3092
+ properties: {
3093
+ capabilityId: {
3094
+ type: "string",
3095
+ description: "Action `capabilityId` or `procedureId`, verbatim from a discover result."
3096
+ },
3097
+ instanceId: {
3098
+ type: "string",
3099
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3100
+ },
3101
+ // Typed, and not merely described: an untyped property is the one position
3102
+ // a provider's constrained decoder cannot constrain, so the model falls
3103
+ // back to its prior — a JSON-encoded string, the shape
3104
+ // `function_call.arguments` carries — and sorts the rest of the capability's
3105
+ // arguments into the sibling modifiers below. `type: "object"` costs
3106
+ // nothing in practice: direct mode already passes `act.inputSchema`
3107
+ // straight through as the tool schema, and providers require that to be an
3108
+ // object schema at the top level. No `additionalProperties` here — the
3109
+ // capability's own schema governs what goes inside.
3110
+ input: {
3111
+ type: "object",
3112
+ description: "Arguments matching that capability's `inputSchema`, as a JSON object \u2014 not a JSON-encoded string. Everything the capability declares goes in here, never beside it."
3113
+ },
3114
+ invocationId: {
3115
+ type: "string",
3116
+ description: "Reuse a previous call's id to retry without executing twice; required when resuming after CONFIRMATION_REQUIRED."
3117
+ },
3118
+ confirmationId: {
3119
+ type: "string",
3120
+ description: "The id returned with CONFIRMATION_REQUIRED, sent back after the user approves."
3121
+ },
3122
+ surfaceVersion: {
3123
+ type: "string",
3124
+ description: "The `surfaceVersion` you planned against. Send it for destructive or externally-visible calls: a surface that moved underneath the plan then fails instead of executing. Omitted, the call binds to what is live now."
3125
+ }
3126
+ },
3127
+ required: ["capabilityId"],
3128
+ additionalProperties: false
3129
+ };
3058
3130
  function describePrefix(plane, effect, confirmation) {
3059
3131
  const parts = [plane, effect];
3060
3132
  if (confirmation === "required") parts.push("requires confirmation");
3061
3133
  return `[${parts.join(" \xB7 ")}]`;
3062
3134
  }
3063
- function legacyDescription(prefix, description, state) {
3064
- const unavailable = state.available ? "" : ` [currently unavailable${state.unavailableReason ? `: ${state.unavailableReason}` : ""}]`;
3065
- const note = state.note ? ` ${state.note}` : "";
3066
- return `${prefix}${unavailable} ${description}${note}`;
3067
- }
3068
3135
  function availabilityState(descriptor) {
3069
3136
  return {
3070
3137
  available: descriptor.available,
@@ -3085,7 +3152,8 @@ function createAgentToolset(registry, options) {
3085
3152
  );
3086
3153
  }
3087
3154
  const confirmationsMode = options.confirmations ?? (options.topology === "remote" ? "two-phase" : "wait");
3088
- const descriptionIncludesState = options.descriptionIncludesState ?? true;
3155
+ const devWarn = registry[DEV_WARN] ?? (() => {
3156
+ });
3089
3157
  const listeners = /* @__PURE__ */ new Set();
3090
3158
  const pendingWaits = /* @__PURE__ */ new Set();
3091
3159
  let disposed = false;
@@ -3199,7 +3267,7 @@ function createAgentToolset(registry, options) {
3199
3267
  void 0,
3200
3268
  describePrefix("domain", proc.effect, proc.confirmation),
3201
3269
  // The stable half only: a contextual note travels in `state.note`.
3202
- stableDescriptionOf(proc),
3270
+ proc.description,
3203
3271
  proc.inputSchema,
3204
3272
  availabilityState(proc),
3205
3273
  needsSuffix ? proc.context?.instanceId ?? proc.registrationId.replace(/[^A-Za-z0-9_-]/g, "") : void 0
@@ -3208,13 +3276,22 @@ function createAgentToolset(registry, options) {
3208
3276
  const assignment = assignWireNames(pending.map((p) => p.wire));
3209
3277
  const tools = pending.map((p, i) => ({
3210
3278
  name: assignment.names[i],
3211
- description: descriptionIncludesState ? legacyDescription(p.prefix, p.description, p.state) : `${p.prefix} ${p.description}`,
3279
+ description: `${p.prefix} ${p.description}`,
3212
3280
  inputSchema: p.inputSchema,
3213
3281
  state: p.state,
3214
3282
  execute: (input, call) => invokeThroughSurface(p.entry, input, call.toolCallId)
3215
3283
  }));
3216
3284
  return { tools, wireNames: assignment.byName };
3217
3285
  }
3286
+ function envelopeFailure(metaCapabilityId, capabilityId, error, toolCallId) {
3287
+ return {
3288
+ status: "error",
3289
+ invocationId: toolCallId ?? `inv_${randomBase62(12)}`,
3290
+ capabilityId: typeof capabilityId === "string" && capabilityId.length > 0 ? capabilityId : metaCapabilityId,
3291
+ error,
3292
+ surfaceVersion: registry.getVersion()
3293
+ };
3294
+ }
3218
3295
  function buildMetaTools() {
3219
3296
  const snapshotFor = () => registry.snapshot({
3220
3297
  consumer: options.consumer,
@@ -3224,21 +3301,12 @@ function createAgentToolset(registry, options) {
3224
3301
  {
3225
3302
  name: "surface_discover",
3226
3303
  description: "[meta] Discover the current agent surface: components, capabilities, procedures, availability, schemas.",
3227
- inputSchema: {
3228
- type: "object",
3229
- properties: {
3230
- scope: {
3231
- type: "array",
3232
- items: { type: "string" },
3233
- // No enum: valid tokens are live component types, and inlining
3234
- // them would make this tool block churn on every mount —
3235
- // the churn AS-META-005 and D28 exist to prevent.
3236
- description: 'Component-type prefixes to narrow the result, e.g. ["devices.table"], taken from `components[].type` of an earlier call \u2014 omit on the first. Narrows only: prefixes outside this host\'s configured scope match nothing and come back in `scopeRejected`.'
3237
- }
3238
- },
3239
- additionalProperties: false
3240
- },
3241
- async execute(input) {
3304
+ inputSchema: META_DISCOVER_SCHEMA,
3305
+ async execute(input, call) {
3306
+ const invalid = validateEnvelope("surface_discover", META_DISCOVER_SCHEMA, input);
3307
+ if (invalid) {
3308
+ return envelopeFailure("meta:surface.discover", void 0, invalid, call.toolCallId);
3309
+ }
3242
3310
  const requested = input?.scope;
3243
3311
  const effective = intersectScope(options.scope, requested);
3244
3312
  const snapshot = registry.snapshot({
@@ -3266,25 +3334,15 @@ function createAgentToolset(registry, options) {
3266
3334
  {
3267
3335
  name: "surface_read",
3268
3336
  description: "[meta] Invoke an observation by capabilityId and return its output.",
3269
- inputSchema: {
3270
- type: "object",
3271
- properties: {
3272
- capabilityId: {
3273
- type: "string",
3274
- description: "Observation id, verbatim from `observations[].capabilityId` in a discover result."
3275
- },
3276
- instanceId: {
3277
- type: "string",
3278
- description: "Only when several components share a type: `components[].instanceId` picks one."
3279
- }
3280
- },
3281
- required: ["capabilityId"],
3282
- additionalProperties: false
3283
- },
3337
+ inputSchema: META_READ_SCHEMA,
3284
3338
  async execute(input, call) {
3285
- const req = input;
3339
+ const req = input ?? {};
3340
+ const invalid = validateEnvelope("surface_read", META_READ_SCHEMA, input);
3341
+ if (invalid) {
3342
+ return envelopeFailure("meta:surface.read", req.capabilityId, invalid, call.toolCallId);
3343
+ }
3286
3344
  const snapshot = snapshotFor();
3287
- const registrationId = findRegistrationId(snapshot, req.capabilityId, req.instanceId);
3345
+ const { registrationId } = findTarget(snapshot, req.capabilityId, req.instanceId);
3288
3346
  return invokeThroughSurface(
3289
3347
  {
3290
3348
  capabilityId: req.capabilityId,
@@ -3302,38 +3360,27 @@ function createAgentToolset(registry, options) {
3302
3360
  {
3303
3361
  name: "surface_act",
3304
3362
  description: "[meta] Invoke an action or procedure by capabilityId. Echo the surfaceVersion you discovered so a surface that changed underneath a destructive plan is rejected rather than executed.",
3305
- inputSchema: {
3306
- type: "object",
3307
- properties: {
3308
- capabilityId: {
3309
- type: "string",
3310
- description: "Action `capabilityId` or `procedureId`, verbatim from a discover result."
3311
- },
3312
- instanceId: {
3313
- type: "string",
3314
- description: "Only when several components share a type: `components[].instanceId` picks one."
3315
- },
3316
- input: { description: "Arguments matching that capability's `inputSchema`." },
3317
- invocationId: {
3318
- type: "string",
3319
- description: "Reuse a previous call's id to retry without executing twice; required when resuming after CONFIRMATION_REQUIRED."
3320
- },
3321
- confirmationId: {
3322
- type: "string",
3323
- description: "The id returned with CONFIRMATION_REQUIRED, sent back after the user approves."
3324
- },
3325
- surfaceVersion: {
3326
- type: "string",
3327
- description: "The `surfaceVersion` you planned against. Send it for destructive or externally-visible calls: a surface that moved underneath the plan then fails instead of executing. Omitted, the call binds to what is live now."
3328
- }
3329
- },
3330
- required: ["capabilityId"],
3331
- additionalProperties: false
3332
- },
3363
+ inputSchema: META_ACT_SCHEMA,
3333
3364
  async execute(input, call) {
3334
- const req = input;
3365
+ const req = input ?? {};
3366
+ const invalid = validateEnvelope("surface_act", META_ACT_SCHEMA, input, ["input"]);
3367
+ if (invalid) {
3368
+ return envelopeFailure("meta:surface.act", req.capabilityId, invalid, call.toolCallId);
3369
+ }
3335
3370
  const snapshot = snapshotFor();
3336
- const registrationId = findRegistrationId(snapshot, req.capabilityId, req.instanceId);
3371
+ const { registrationId, inputSchema } = findTarget(
3372
+ snapshot,
3373
+ req.capabilityId,
3374
+ req.instanceId
3375
+ );
3376
+ let actInput = req.input;
3377
+ const parsed = parseStringifiedObject(actInput, inputSchema);
3378
+ if (parsed !== void 0) {
3379
+ actInput = parsed;
3380
+ devWarn(
3381
+ `[agent-surface] surface_act got \`input\` as a JSON-encoded string for "${req.capabilityId}" and parsed it; the provider is not honoring the tool schema.`
3382
+ );
3383
+ }
3337
3384
  return invokeThroughSurface(
3338
3385
  {
3339
3386
  capabilityId: req.capabilityId,
@@ -3342,7 +3389,7 @@ function createAgentToolset(registry, options) {
3342
3389
  surfaceVersion: req.surfaceVersion ?? snapshot.surfaceVersion,
3343
3390
  kind: "action"
3344
3391
  },
3345
- req.input,
3392
+ actInput,
3346
3393
  call.toolCallId,
3347
3394
  {
3348
3395
  ...req.invocationId !== void 0 ? { invocationId: req.invocationId } : {},
@@ -3436,7 +3483,7 @@ function intersectScope(floor, requested) {
3436
3483
  }
3437
3484
  return out.size > 0 ? { scope: [...out], empty: false, rejected } : { empty: true, rejected };
3438
3485
  }
3439
- function findRegistrationId(snapshot, capabilityId, instanceId) {
3486
+ function findTarget(snapshot, capabilityId, instanceId) {
3440
3487
  const matches = [];
3441
3488
  for (const component of snapshot.components) {
3442
3489
  if (instanceId !== void 0 && component.instanceId !== instanceId) continue;
@@ -3444,12 +3491,89 @@ function findRegistrationId(snapshot, capabilityId, instanceId) {
3444
3491
  ...component.observations,
3445
3492
  ...component.actions
3446
3493
  ];
3447
- if (all.some((c) => c.capabilityId === capabilityId)) matches.push(component.registrationId);
3494
+ const hit = all.find((c) => c.capabilityId === capabilityId);
3495
+ if (hit) {
3496
+ matches.push({
3497
+ registrationId: component.registrationId,
3498
+ ..."inputSchema" in hit ? { inputSchema: hit.inputSchema } : {}
3499
+ });
3500
+ }
3448
3501
  }
3449
3502
  for (const proc of snapshot.procedures) {
3450
- if (proc.procedureId === capabilityId) matches.push(proc.registrationId);
3503
+ if (proc.procedureId === capabilityId) {
3504
+ matches.push({ registrationId: proc.registrationId, inputSchema: proc.inputSchema });
3505
+ }
3506
+ }
3507
+ return matches.length === 1 ? matches[0] : {};
3508
+ }
3509
+ function validateEnvelope(verb, schema, raw, exempt = []) {
3510
+ if (raw !== void 0 && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
3511
+ return envelopeError(verb, [
3512
+ { path: "", message: `\`${verb}\` takes a JSON object of arguments.` }
3513
+ ]);
3514
+ }
3515
+ const properties = schema.properties ?? {};
3516
+ const known = Object.keys(properties);
3517
+ const required = schema.required ?? [];
3518
+ const req = raw ?? {};
3519
+ const issues = [];
3520
+ for (const key of required) {
3521
+ if (req[key] === void 0) issues.push({ path: key, message: `\`${key}\` is required.` });
3522
+ }
3523
+ for (const [key, value] of Object.entries(req)) {
3524
+ if (value === void 0) continue;
3525
+ if (!known.includes(key)) {
3526
+ if (schema.additionalProperties === false) {
3527
+ issues.push({
3528
+ path: key,
3529
+ // The high-value half is the pointer back at `input`: it turns the
3530
+ // dead end of a hoisted capability argument into a one-retry
3531
+ // recovery. A verb without an `input` has nowhere to point.
3532
+ message: `Unknown top-level property. \`${verb}\` accepts only ${known.join(", ")}.${known.includes("input") ? " An argument the capability declares belongs inside `input`." : ""}`
3533
+ });
3534
+ }
3535
+ continue;
3536
+ }
3537
+ if (exempt.includes(key)) continue;
3538
+ const issue = checkDeclaredType(key, properties[key], value);
3539
+ if (issue) issues.push(issue);
3540
+ }
3541
+ return issues.length > 0 ? envelopeError(verb, issues) : void 0;
3542
+ }
3543
+ function checkDeclaredType(key, property, value) {
3544
+ if (property.type === "string" && (typeof value !== "string" || value.length === 0)) {
3545
+ return { path: key, message: `\`${key}\` must be a non-empty string.` };
3546
+ }
3547
+ if (property.type === "array") {
3548
+ if (!Array.isArray(value)) return { path: key, message: `\`${key}\` must be an array.` };
3549
+ const items = property.items;
3550
+ if (items?.type === "string" && !value.every((item) => typeof item === "string")) {
3551
+ return { path: key, message: `\`${key}\` must be an array of strings.` };
3552
+ }
3553
+ }
3554
+ return void 0;
3555
+ }
3556
+ function envelopeError(verb, issues) {
3557
+ return {
3558
+ code: "INVALID_INPUT",
3559
+ // Names the envelope, not the capability: pointing the model at the
3560
+ // capability's schema when the wrapper is what is wrong sends it to fix
3561
+ // something that is already correct.
3562
+ message: `The \`${verb}\` call is malformed \u2014 the fault is in the tool's own arguments, not the capability's input. Fix the listed issues and retry.`,
3563
+ retry: "with-changes",
3564
+ details: { issues }
3565
+ };
3566
+ }
3567
+ function parseStringifiedObject(value, targetSchema) {
3568
+ if (typeof value !== "string" || targetSchema?.type !== "object") return void 0;
3569
+ let parsed;
3570
+ try {
3571
+ parsed = JSON.parse(value);
3572
+ } catch {
3573
+ return void 0;
3451
3574
  }
3452
- return matches.length === 1 ? matches[0] : void 0;
3575
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
3576
+ return parsed;
3453
3577
  }
3454
3578
  export {
3455
3579
  AGENT_CAPABILITY_ERROR_CODES,
@@ -3490,7 +3614,6 @@ export {
3490
3614
  parseCapabilityId,
3491
3615
  rateLimit,
3492
3616
  requireConfirmation,
3493
- stableDescriptionOf,
3494
3617
  tenantBoundary,
3495
3618
  validateComponentDefinition,
3496
3619
  validateJsonSchemaDocument,