@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 +14 -1
- package/README.md +3 -0
- package/THREAT_MODEL.md +6 -0
- package/dist/runner.mjs +35 -6
- package/docs/security-boundary.md +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.1.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|