@synapsor/runner 1.1.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 1.1.0 (prepared, not published)
3
+ ## 1.1.1 (prepared, not published)
4
+
5
+ ### Resource Read Authorization
6
+
7
+ - Reauthorizes local MCP proposal, evidence, and replay reads against the
8
+ owning capability's trusted tenant and principal before returning content.
9
+ - Returns the same generic `RESOURCE_NOT_FOUND` result for missing resources,
10
+ cross-tenant access, cross-principal access, and incomplete legacy ownership
11
+ metadata so a leaked handle does not become bearer authority.
12
+ - Adds shared-store and Streamable HTTP regressions proving owner access still
13
+ works while cross-session resource reads fail closed.
14
+ - Stages only `@synapsor/runner@1.1.1`; canonical Spec and DSL remain `1.1.0`.
15
+
16
+ ## 1.1.0
4
17
 
5
18
  ### Fleet Safety And Operations
6
19
 
package/README.md CHANGED
@@ -161,6 +161,9 @@ trust boundaries, covered threats, non-goals, and required operator controls.
161
161
  - [Conformance fixtures](docs/conformance.md) prove trusted context, scoped
162
162
  reads, kept-out fields, proposal boundaries, approval, receipts, and replay
163
163
  behavior rather than only validating JSON shape.
164
+ - MCP proposal, evidence, and replay handles are references rather than bearer
165
+ authority: resource reads re-check the owning tenant and principal against
166
+ the current trusted session.
164
167
  - `corepack pnpm test:live-apply` runs disposable Postgres and MySQL scenarios.
165
168
  It proves source rows stay unchanged before approval, guarded writeback
166
169
  applies once, idempotent retry does not duplicate the effect, and a stale row
package/THREAT_MODEL.md CHANGED
@@ -59,6 +59,10 @@ idempotency boundary for effects.
59
59
  - Model-callable approval: approval/commit tools are not exposed to MCP clients by default.
60
60
  - Claims/environment confusion: an `http_claims` server fails before serving
61
61
  if a capability resolves an environment/static contract context.
62
+ - Leaked local resource handle: proposal, evidence, and replay reads resolve
63
+ the owning capability context again and require the same trusted tenant and
64
+ principal; missing or mismatched ownership returns the same generic
65
+ `RESOURCE_NOT_FOUND` response.
62
66
  - JWT algorithm/key confusion: networked sessions use an explicit RS256/ES256
63
67
  allowlist, issuer/audience/time checks, `kid`, and bounded public-key/JWKS
64
68
  loading.
@@ -98,6 +102,8 @@ idempotency boundary for effects.
98
102
  configured `max_entries` safety bound.
99
103
  - Use verified `signed_key` or `jwt_oidc` reviewers for production-like shared
100
104
  queues; `dev_env` is unverified.
105
+ - Treat proposal/evidence/replay handles as identifiers, not authorization;
106
+ preserve verified per-session context on every networked resource read.
101
107
 
102
108
  ## Release Blockers
103
109
 
package/dist/runner.mjs CHANGED
@@ -7259,7 +7259,7 @@ function createMcpRuntime(config, options = {}) {
7259
7259
  },
7260
7260
  readResource: async (uri) => {
7261
7261
  assertStoreAvailable();
7262
- return readLocalResource(store, uri);
7262
+ return readLocalResource(store, uri, config, env, trustedContext);
7263
7263
  },
7264
7264
  poolMetrics: () => resources.poolMetrics(),
7265
7265
  rateLimitMetrics: () => resources.rateLimitMetrics(),
@@ -9253,7 +9253,7 @@ function diffFromChangeSet(changeSet) {
9253
9253
  }
9254
9254
  return diff;
9255
9255
  }
9256
- async function readLocalResource(store, uri) {
9256
+ async function readLocalResource(store, uri, config, env, trustedContext) {
9257
9257
  const parsed = new URL(uri);
9258
9258
  const parts = parsed.pathname.split("/").filter(Boolean);
9259
9259
  const collection = parsed.hostname;
@@ -9261,19 +9261,48 @@ async function readLocalResource(store, uri) {
9261
9261
  if (!id) throw new McpRuntimeError("RESOURCE_ID_MISSING", `Resource id missing in ${uri}.`);
9262
9262
  if (collection === "proposals") {
9263
9263
  const proposal = await store.getProposal(id);
9264
- if (!proposal) throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Proposal not found: ${id}`);
9264
+ if (!proposal) throw localResourceNotFound();
9265
+ assertLocalResourceAccess(config, env, trustedContext, {
9266
+ tenant_id: proposal.tenant_id,
9267
+ principal: proposal.principal ?? proposal.change_set.principal.id,
9268
+ capability: proposal.capability ?? proposal.action
9269
+ });
9265
9270
  return { proposal, events: await store.events(id), receipts: await store.receipts(id) };
9266
9271
  }
9267
9272
  if (collection === "evidence") {
9268
9273
  const evidence2 = await store.getEvidenceBundle(id);
9269
- if (!evidence2) throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Evidence bundle not found: ${id}`);
9274
+ if (!evidence2) throw localResourceNotFound();
9275
+ assertLocalResourceAccess(config, env, trustedContext, {
9276
+ tenant_id: evidence2.tenant_id,
9277
+ principal: evidence2.principal,
9278
+ capability: evidence2.capability
9279
+ });
9270
9280
  return evidence2;
9271
9281
  }
9272
9282
  if (collection === "replay") {
9273
9283
  const proposalId = id.startsWith("replay_") ? id.slice("replay_".length) : id;
9284
+ const proposal = await store.getProposal(proposalId);
9285
+ if (!proposal) throw localResourceNotFound();
9286
+ assertLocalResourceAccess(config, env, trustedContext, {
9287
+ tenant_id: proposal.tenant_id,
9288
+ principal: proposal.principal ?? proposal.change_set.principal.id,
9289
+ capability: proposal.capability ?? proposal.action
9290
+ });
9274
9291
  return await store.replay(proposalId);
9275
9292
  }
9276
- throw new McpRuntimeError("RESOURCE_NOT_FOUND", `Unsupported Synapsor resource: ${uri}`);
9293
+ throw localResourceNotFound();
9294
+ }
9295
+ function assertLocalResourceAccess(config, env, trustedContext, owner) {
9296
+ if (!owner.tenant_id || !owner.principal || !owner.capability) throw localResourceNotFound();
9297
+ const capability = localCapabilities(config).find((item) => item.name === owner.capability);
9298
+ if (!capability) throw localResourceNotFound();
9299
+ const context = resolveTrustedContext(config, env, capability, trustedContext);
9300
+ if (context.tenant_id !== owner.tenant_id || context.principal !== owner.principal) {
9301
+ throw localResourceNotFound();
9302
+ }
9303
+ }
9304
+ function localResourceNotFound() {
9305
+ return new McpRuntimeError("RESOURCE_NOT_FOUND", "Synapsor resource not found.");
9277
9306
  }
9278
9307
  async function resourceResult(uri, reader) {
9279
9308
  const payload = await reader(uri);
@@ -12585,7 +12614,7 @@ function isRecord7(value) {
12585
12614
  // apps/runner/package.json
12586
12615
  var package_default = {
12587
12616
  name: "@synapsor/runner",
12588
- version: "1.1.0",
12617
+ version: "1.1.1",
12589
12618
  description: "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
12590
12619
  license: "Apache-2.0",
12591
12620
  type: "module",
@@ -37,6 +37,13 @@ Trusted context comes from local configuration, environment bindings, or Cloud
37
37
  session context in Cloud mode. Tenant, principal, and authorization scope must
38
38
  not be accepted from the model as authority.
39
39
 
40
+ Proposal, evidence, and replay handles are references, not bearer credentials.
41
+ Before returning a local MCP resource, Runner resolves the resource's owning
42
+ capability context again and requires both tenant and principal to match the
43
+ current trusted session. Missing resources and ownership mismatches return the
44
+ same generic `RESOURCE_NOT_FOUND` result. Local operator CLI access remains an
45
+ explicit host-level administrative boundary.
46
+
40
47
  Proposal tools read the current row through the read credential, store evidence
41
48
  and an exact before/after diff, and leave the source database unchanged.
42
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapsor/runner",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",