@zackbart/connecta 0.39.0 → 0.41.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/CHANGELOG.md +115 -0
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/native.js +10 -0
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +151 -37
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -7
- package/dist/connectors/remote-mcp/oauth.js +6 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +37 -8
- package/dist/connectors/remote-mcp.js +8 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +77 -35
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +58 -36
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +12 -5
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider-call-facts.js +6 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +15 -0
- package/dist/runtime/request-reads.js +87 -10
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +85 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +83 -2
- package/documentation/auth.md +132 -37
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/integrating.md +2 -2
- package/documentation/meta-tools.md +134 -15
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- package/templates/node/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkedActivityOperation } from "../operation-activity.js";
|
|
1
2
|
import { recordedToolName } from "../operator-record.js";
|
|
2
3
|
/** A recorded tool is shown only when its name passes the record grammar and its endpoint granted it. */
|
|
3
4
|
function activityToolVisible(access, event) {
|
|
@@ -15,6 +16,31 @@ function activityToolVisible(access, event) {
|
|
|
15
16
|
* pool that is no longer declared is withheld.
|
|
16
17
|
*/
|
|
17
18
|
export function activityEventVisible(context, authz, registry, event) {
|
|
19
|
+
const pool = event.pool !== undefined ? context.opts.pools.get(event.pool) : undefined;
|
|
20
|
+
if (event.pool !== undefined && !pool)
|
|
21
|
+
return false;
|
|
22
|
+
const access = pool ? pool.access : { connectorIds: "all" };
|
|
23
|
+
if (event.kind === "operation") {
|
|
24
|
+
const operation = checkedActivityOperation(event.operation);
|
|
25
|
+
if (!operation ||
|
|
26
|
+
event.toolName !== operation.name ||
|
|
27
|
+
event.connectorId !== (operation.connectors.length === 1 ? operation.connectors[0] : "$operation"))
|
|
28
|
+
return false;
|
|
29
|
+
const ownerOnly = operation.personal ||
|
|
30
|
+
event.actorBasis === "principal" ||
|
|
31
|
+
operation.connectors.some((id) => registry.getConnector(id)?.authScope === "personal");
|
|
32
|
+
if (ownerOnly) {
|
|
33
|
+
const owner = authz.identity.principal;
|
|
34
|
+
if (!owner ||
|
|
35
|
+
event.actorBasis !== "principal" ||
|
|
36
|
+
event.actor.id !== owner.id ||
|
|
37
|
+
event.actor.namespace !== owner.namespace)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return operation.connectors.every((id) => registry.canReadConnectorSkills(id) &&
|
|
41
|
+
(access.connectorIds === "all" || access.connectorIds.includes(id)) &&
|
|
42
|
+
!access.toolAccess?.has(id));
|
|
43
|
+
}
|
|
18
44
|
const connector = registry.getConnector(event.connectorId);
|
|
19
45
|
if (!connector)
|
|
20
46
|
return false;
|
|
@@ -26,10 +52,6 @@ export function activityEventVisible(context, authz, registry, event) {
|
|
|
26
52
|
event.actor.namespace !== owner.namespace)
|
|
27
53
|
return false;
|
|
28
54
|
}
|
|
29
|
-
const pool = event.pool !== undefined ? context.opts.pools.get(event.pool) : undefined;
|
|
30
|
-
if (event.pool !== undefined && !pool)
|
|
31
|
-
return false;
|
|
32
|
-
const access = pool ? pool.access : { connectorIds: "all" };
|
|
33
55
|
if (access.connectorIds !== "all" && !access.connectorIds.includes(event.connectorId))
|
|
34
56
|
return false;
|
|
35
57
|
// Connector-wide counts reveal tools outside an address-only grant.
|
package/dist/routes/activity.js
CHANGED
|
@@ -27,12 +27,13 @@ function enrichActivityActorLabels(page, auth) {
|
|
|
27
27
|
...(event.actor.namespace ? { namespace: event.actor.namespace } : {}),
|
|
28
28
|
};
|
|
29
29
|
// Re-check stored client facts, including custom readers and old rows.
|
|
30
|
-
const { packageVersion: suppliedPackageVersion, clientName: suppliedName, clientVersion: suppliedVersion, classification: _classification, resultBytes: _resultBytes, kind: _kind, drift: _drift, pool: _pool, actorBasis: _actorBasis, ...record } = event;
|
|
30
|
+
const { packageVersion: suppliedPackageVersion, clientName: suppliedName, clientVersion: suppliedVersion, classification: _classification, resultBytes: _resultBytes, kind: _kind, drift: _drift, operation: _operation, friction: suppliedFriction, pool: _pool, actorBasis: _actorBasis, ...record } = event;
|
|
31
31
|
const packageVersion = activityPackageVersion(suppliedPackageVersion);
|
|
32
32
|
const clientName = activityClientFact(suppliedName, "name");
|
|
33
33
|
const clientVersion = activityClientFact(suppliedVersion, "version");
|
|
34
34
|
return {
|
|
35
35
|
...record,
|
|
36
|
+
...(event.kind !== "operation" && suppliedFriction !== undefined ? { friction: suppliedFriction } : {}),
|
|
36
37
|
...activityBehaviorFacts(event),
|
|
37
38
|
...(packageVersion !== undefined ? { packageVersion } : {}),
|
|
38
39
|
...(clientName !== undefined ? { clientName } : {}),
|
package/dist/routes/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { startTimingTrace } from "../timing-trace.js";
|
|
1
2
|
import { routeNames } from "../agent-routes.js";
|
|
2
3
|
import { upwardStep } from "../upward.js";
|
|
3
4
|
import { bindDiscoveryEndpoint } from "../discovery-health.js";
|
|
@@ -128,7 +129,7 @@ function withMcpCors(response, request, allowedOrigin) {
|
|
|
128
129
|
}
|
|
129
130
|
headers.append("Vary", "Access-Control-Request-Headers");
|
|
130
131
|
}
|
|
131
|
-
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery");
|
|
132
|
+
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery, Connecta-Next-Action");
|
|
132
133
|
const wrapped = new Response(response.body, {
|
|
133
134
|
status: response.status,
|
|
134
135
|
statusText: response.statusText,
|
|
@@ -359,10 +360,11 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
359
360
|
// A request-local server cannot publish catalog changes. Set this before
|
|
360
361
|
// tool registration, whose SDK default otherwise advertises listChanged.
|
|
361
362
|
capabilities: { tools: { listChanged: false }, extensions: { "io.modelcontextprotocol/skills": {} } },
|
|
362
|
-
// Instructions
|
|
363
|
-
// description. Every cache hint below is private, so a client keeps
|
|
363
|
+
// Instructions reflect the admitted pool and effective write policy. Every cache hint below is private, so a client keeps
|
|
364
364
|
// what it learned per endpoint URL and never shares it across them.
|
|
365
|
-
instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined, native
|
|
365
|
+
instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined, native, readOnly ||
|
|
366
|
+
(registry.listConnectors().length > 0 &&
|
|
367
|
+
registry.listConnectors().every((connector) => connector.readOnly === true))),
|
|
366
368
|
inputRequired: { legacyShim: false },
|
|
367
369
|
requestState: { verify: (state, context) => authElicitation.verify(state, context) },
|
|
368
370
|
cacheHints: {
|
|
@@ -378,6 +380,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
378
380
|
? {
|
|
379
381
|
sink: opts.config.activity?.store,
|
|
380
382
|
recordTool: opts.config.activity?.recordTool,
|
|
383
|
+
recordOperation: opts.config.activity?.recordOperation,
|
|
381
384
|
actor,
|
|
382
385
|
...(principalActor ? { principalActor } : {}),
|
|
383
386
|
requestId: crypto.randomUUID(),
|
|
@@ -635,16 +638,22 @@ export function createMcpRoute(opts) {
|
|
|
635
638
|
request.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
636
639
|
if (request.signal.aborted)
|
|
637
640
|
onCallerAbort();
|
|
641
|
+
const finishTiming = startTimingTrace(requestScope, opts.config.timingTraceSampleRate, opts.config.logger);
|
|
638
642
|
let deadlineTimer;
|
|
639
643
|
return scopedToBody(Effect.gen(function* () {
|
|
640
644
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
645
|
+
try {
|
|
646
|
+
requestOwner(requestScope).close();
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
finishTiming?.();
|
|
650
|
+
if (!localAbort.signal.aborted) {
|
|
651
|
+
localAbort.abort(request.signal.reason ?? new Error("MCP request ended."));
|
|
652
|
+
}
|
|
653
|
+
request.signal.removeEventListener("abort", onCallerAbort);
|
|
654
|
+
if (deadlineTimer !== undefined)
|
|
655
|
+
clearTimeout(deadlineTimer);
|
|
644
656
|
}
|
|
645
|
-
request.signal.removeEventListener("abort", onCallerAbort);
|
|
646
|
-
if (deadlineTimer !== undefined)
|
|
647
|
-
clearTimeout(deadlineTimer);
|
|
648
657
|
}));
|
|
649
658
|
// A request naming `subscriptions/listen` defers its permit: nothing of
|
|
650
659
|
// its body is read until auth and endpoint admission pass, and then at
|
|
@@ -652,7 +661,9 @@ export function createMcpRoute(opts) {
|
|
|
652
661
|
// modern listen goes without a permit; trusting the header alone would
|
|
653
662
|
// let a legacy tools/call bypass the pool, so anything else takes one
|
|
654
663
|
// after classification. Every other request takes its permit here,
|
|
655
|
-
// before auth, and no body
|
|
664
|
+
// before auth, and this route reads no body before auth on any path.
|
|
665
|
+
// The Node adapter has already buffered up to its own pre-auth cap
|
|
666
|
+
// (`listen`'s `maxBodyBytes`) before a request reaches core.
|
|
656
667
|
const startedAt = Date.now();
|
|
657
668
|
const maxDurationMs = opts.requestAdmission.maxDurationMs;
|
|
658
669
|
const listenHeader = request.method === "POST" && request.headers.get("Mcp-Method") === "subscriptions/listen";
|
|
@@ -766,8 +777,12 @@ export function createMcpRoute(opts) {
|
|
|
766
777
|
const headers = new Headers(response.headers);
|
|
767
778
|
headers.set("Connecta-Error-Code", "host_auth_required");
|
|
768
779
|
headers.set("Connecta-Recovery", "host_connection");
|
|
769
|
-
|
|
770
|
-
|
|
780
|
+
headers.set("Connecta-Next-Action", JSON.stringify({
|
|
781
|
+
action: "reconnect_host",
|
|
782
|
+
purpose: "Reconnect the host with valid inbound credentials and endpoint grants; downstream OAuth cannot repair this connection.",
|
|
783
|
+
}));
|
|
784
|
+
// Custom adapters own their response stream and request lifetime.
|
|
785
|
+
// Their typed recovery is carried in headers instead of replacing it.
|
|
771
786
|
if (!response.headers.get("Content-Type")?.includes("application/json")) {
|
|
772
787
|
return cors(new Response(response.body, { status: response.status, headers }));
|
|
773
788
|
}
|
|
@@ -780,6 +795,10 @@ export function createMcpRoute(opts) {
|
|
|
780
795
|
message: "The host is not authorized to connect to this endpoint. Sign in or update the host's connecta access token and endpoint grants.",
|
|
781
796
|
retryable: false,
|
|
782
797
|
recovery: "host_connection",
|
|
798
|
+
nextAction: {
|
|
799
|
+
action: "reconnect_host",
|
|
800
|
+
purpose: "Reconnect this host to Connecta with valid inbound credentials and endpoint grants. Downstream connector authorization cannot repair the host connection.",
|
|
801
|
+
},
|
|
783
802
|
},
|
|
784
803
|
})), { status: response.status, headers }));
|
|
785
804
|
}
|
package/dist/routes/ui-config.js
CHANGED
|
@@ -171,7 +171,7 @@ export function scanOperatorActivity(context, authz, registry, rows, agents) {
|
|
|
171
171
|
continue;
|
|
172
172
|
// A last call is a call; any row recorded for an agent, a catalog
|
|
173
173
|
// refresh included, is a sign it was seen.
|
|
174
|
-
const row = event.kind
|
|
174
|
+
const row = event.kind === undefined && OUTCOMES.has(event.outcome) ? byId.get(event.connectorId) : undefined;
|
|
175
175
|
const agent = (event.actor.kind === "access_token" || event.actor.kind === "vercel_oidc") &&
|
|
176
176
|
event.actor.namespace === AGENT_NAMESPACE &&
|
|
177
177
|
event.actor.id
|
package/dist/routes/ui-data.js
CHANGED
|
@@ -249,6 +249,7 @@ export function uiData(registry, baseUrl, options) {
|
|
|
249
249
|
toolCount,
|
|
250
250
|
observedAt: Date.now(),
|
|
251
251
|
}));
|
|
252
|
+
const observedAgeMs = observation ? Math.max(0, Date.now() - observation.observedAt) : undefined;
|
|
252
253
|
return {
|
|
253
254
|
id: c.id,
|
|
254
255
|
authScope: c.authScope ?? "shared",
|
|
@@ -271,9 +272,9 @@ export function uiData(registry, baseUrl, options) {
|
|
|
271
272
|
...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
|
|
272
273
|
catalogAgeMs: registry.catalogAgeMs(c.id),
|
|
273
274
|
lastCall: null,
|
|
274
|
-
...(
|
|
275
|
+
...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
|
|
275
276
|
},
|
|
276
|
-
...(
|
|
277
|
+
...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
|
|
277
278
|
// Counts only, and only when a refresh in this runtime produced them.
|
|
278
279
|
// `Registry.withCatalogObservations` rebuilt the report through
|
|
279
280
|
...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
|
|
@@ -25,6 +25,14 @@ export function registerNativeStorage(edge, native) {
|
|
|
25
25
|
storageMethods.set(edge, own);
|
|
26
26
|
const getMany = native.getMany;
|
|
27
27
|
storage.set(edge, {
|
|
28
|
+
catalogSnapshot: native.catalogSnapshot
|
|
29
|
+
? (keys, generationKey, prefix, suffixes) => typed(() => getNativeStorage(edge)
|
|
30
|
+
? native.catalogSnapshot(keys, generationKey, prefix, suffixes)
|
|
31
|
+
: Effect.succeed(new Map()))
|
|
32
|
+
: undefined,
|
|
33
|
+
accessToken: native.accessToken
|
|
34
|
+
? (hash) => typed(() => (getNativeStorage(edge) ? native.accessToken(hash) : Effect.succeed(undefined)))
|
|
35
|
+
: undefined,
|
|
28
36
|
get: (key) => current(edge, "get", own.get, [key], () => native.get(key)),
|
|
29
37
|
// A replaced `get` answers each key itself, in order, as before.
|
|
30
38
|
...(getMany
|
|
@@ -34,6 +42,13 @@ export function registerNativeStorage(edge, native) {
|
|
|
34
42
|
: Effect.forEach(keys, (key) => current(edge, "get", own.get, [key], () => native.get(key)))),
|
|
35
43
|
}
|
|
36
44
|
: {}),
|
|
45
|
+
...(native.setMany
|
|
46
|
+
? {
|
|
47
|
+
setMany: (entries, options) => typed(() => getNativeStorage(edge)
|
|
48
|
+
? native.setMany(entries, options)
|
|
49
|
+
: Effect.asVoid(Effect.forEach(entries, ([key, value]) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options))))),
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
37
52
|
set: (key, value, options) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options)),
|
|
38
53
|
delete: (key) => current(edge, "delete", own.delete, [key], () => native.delete(key)),
|
|
39
54
|
list: (prefix) => current(edge, "list", own.list, [prefix], () => native.list(prefix)),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { utf8Length } from "../bytes.js";
|
|
1
2
|
// Request-scoped memo of storage reads (INV-7).
|
|
2
3
|
//
|
|
3
4
|
// One request often reads the same key many times: a catalog listing reads its
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
// `FinalizationRegistry` (workerd before compatibility date 2025-05-05 without
|
|
37
38
|
// `enable_weak_ref`) memoizes nothing: every read goes to storage.
|
|
38
39
|
import { Effect } from "effect";
|
|
40
|
+
import { getNativeStorage } from "./native-storage.js";
|
|
41
|
+
const roots = new WeakMap();
|
|
39
42
|
const observed = new WeakMap();
|
|
40
43
|
/**
|
|
41
44
|
* Open memos holding or reading entries of each store. Weak, so a request
|
|
@@ -72,6 +75,8 @@ export function observedWrite(store, key, write) {
|
|
|
72
75
|
* one is.
|
|
73
76
|
*/
|
|
74
77
|
export function observeStorageWrites(storage, layer) {
|
|
78
|
+
if ("root" in layer)
|
|
79
|
+
roots.set(layer.root, storage);
|
|
75
80
|
observed.set(storage, {
|
|
76
81
|
own: {
|
|
77
82
|
get: storage.get,
|
|
@@ -85,7 +90,7 @@ export function observeStorageWrites(storage, layer) {
|
|
|
85
90
|
return storage;
|
|
86
91
|
}
|
|
87
92
|
/** The underlying store and key a read of `key` reaches, when every layer is observed. */
|
|
88
|
-
function
|
|
93
|
+
export function physicalStorageAddress(storage, key) {
|
|
89
94
|
const entry = observed.get(storage);
|
|
90
95
|
if (!entry)
|
|
91
96
|
return undefined;
|
|
@@ -96,10 +101,17 @@ function physical(storage, key) {
|
|
|
96
101
|
own.list !== storage.list ||
|
|
97
102
|
own.compareAndSet !== storage.compareAndSet)
|
|
98
103
|
return undefined;
|
|
99
|
-
return "root" in layer ? { store: layer.root, key } :
|
|
104
|
+
return "root" in layer ? { store: layer.root, key } : physicalStorageAddress(layer.storage, layer.prefix + key);
|
|
100
105
|
}
|
|
101
106
|
const weakReferences = () => typeof WeakRef === "function" && typeof FinalizationRegistry === "function";
|
|
102
107
|
export class RequestReads {
|
|
108
|
+
trace;
|
|
109
|
+
constructor(trace) {
|
|
110
|
+
this.trace = trace;
|
|
111
|
+
}
|
|
112
|
+
note(fresh, hit, value, started) {
|
|
113
|
+
this.trace?.record(fresh ? "memo.fresh" : hit ? "memo.hit" : "memo.miss", performance.now() - started, value === null ? 0 : utf8Length(value), 0, value === null ? 1 : 0);
|
|
114
|
+
}
|
|
103
115
|
closed = false;
|
|
104
116
|
stores = new Map();
|
|
105
117
|
ref = weakReferences() ? new WeakRef(this) : undefined;
|
|
@@ -126,7 +138,7 @@ export class RequestReads {
|
|
|
126
138
|
}
|
|
127
139
|
/** A memoized answer, or how to record the value a storage read returns. */
|
|
128
140
|
lookup(storage, key, fresh) {
|
|
129
|
-
const address = this.closed || !this.ref ? undefined :
|
|
141
|
+
const address = this.closed || !this.ref ? undefined : physicalStorageAddress(storage, key);
|
|
130
142
|
if (!address)
|
|
131
143
|
return undefined;
|
|
132
144
|
const entries = this.entriesFor(address.store);
|
|
@@ -152,22 +164,87 @@ export class RequestReads {
|
|
|
152
164
|
read(storage, key, load, fresh = false) {
|
|
153
165
|
return Effect.suspend(() => {
|
|
154
166
|
const found = this.lookup(storage, key, fresh);
|
|
155
|
-
if (!found)
|
|
167
|
+
if (!found && !this.trace)
|
|
156
168
|
return load;
|
|
157
|
-
|
|
169
|
+
const started = this.trace ? performance.now() : 0;
|
|
170
|
+
if (found?.hit) {
|
|
171
|
+
if (this.trace)
|
|
172
|
+
this.note(fresh, true, found.value, started);
|
|
158
173
|
return Effect.succeed(found.value);
|
|
159
|
-
|
|
174
|
+
}
|
|
175
|
+
return Effect.tap(load, (value) => Effect.sync(() => {
|
|
176
|
+
found?.record(value);
|
|
177
|
+
if (this.trace)
|
|
178
|
+
this.note(fresh, false, value, started);
|
|
179
|
+
}));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Prefetch only keys whose callers already permit request-local staleness. */
|
|
183
|
+
prefetch(requests, load, linked) {
|
|
184
|
+
return Effect.suspend(() => {
|
|
185
|
+
const pending = requests.map(({ storage, key }) => ({
|
|
186
|
+
address: physicalStorageAddress(storage, key),
|
|
187
|
+
found: this.lookup(storage, key, false),
|
|
188
|
+
}));
|
|
189
|
+
if (this.trace)
|
|
190
|
+
for (const { found } of pending)
|
|
191
|
+
if (found?.hit)
|
|
192
|
+
this.note(false, true, found.value, performance.now());
|
|
193
|
+
const started = this.trace ? performance.now() : 0;
|
|
194
|
+
const root = pending[0]?.address?.store;
|
|
195
|
+
const storage = root && roots.get(root);
|
|
196
|
+
if (!storage || pending.some(({ address, found }) => !found || address?.store !== root))
|
|
197
|
+
return Effect.void;
|
|
198
|
+
if (pending.every(({ found }) => found?.hit))
|
|
199
|
+
return Effect.void;
|
|
200
|
+
const generation = linked && physicalStorageAddress(linked.storage, linked.generationKey);
|
|
201
|
+
const prefix = linked && physicalStorageAddress(linked.storage, linked.prefix);
|
|
202
|
+
const snapshot = getNativeStorage(storage)?.catalogSnapshot;
|
|
203
|
+
if (linked && snapshot && generation?.store === root && prefix?.store === root) {
|
|
204
|
+
// Candidates only locate entries. An intervening external write can make
|
|
205
|
+
// these stale, so consumers retain the separate post-chunk primary fence.
|
|
206
|
+
return Effect.map(snapshot(pending.map(({ address }) => address.key), generation.key, prefix.key, linked.suffixes), (values) => {
|
|
207
|
+
pending.forEach(({ address, found }) => {
|
|
208
|
+
if (found && !found.hit)
|
|
209
|
+
found.record(values.get(address.key) ?? null);
|
|
210
|
+
});
|
|
211
|
+
for (const [key, value] of values) {
|
|
212
|
+
if (pending.some(({ address }) => address.key === key))
|
|
213
|
+
continue;
|
|
214
|
+
const found = this.lookup(storage, key, false);
|
|
215
|
+
if (found && !found.hit)
|
|
216
|
+
found.record(value);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const misses = pending.filter(({ found }) => found && !found.hit);
|
|
221
|
+
if (!misses.length)
|
|
222
|
+
return Effect.void;
|
|
223
|
+
return Effect.map(load(storage, misses.map(({ address }) => address.key)), (values) => {
|
|
224
|
+
misses.forEach(({ found }, i) => {
|
|
225
|
+
if (found && !found.hit) {
|
|
226
|
+
const value = values[i] ?? null;
|
|
227
|
+
found.record(value);
|
|
228
|
+
if (this.trace)
|
|
229
|
+
this.note(false, false, value, started);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
160
233
|
});
|
|
161
234
|
}
|
|
162
235
|
/** `read` for a Promise-shaped caller, without another Effect edge. */
|
|
163
236
|
async readPromise(storage, key, load, fresh = false) {
|
|
164
237
|
const found = this.lookup(storage, key, fresh);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
238
|
+
const started = this.trace ? performance.now() : 0;
|
|
239
|
+
if (found?.hit) {
|
|
240
|
+
if (this.trace)
|
|
241
|
+
this.note(fresh, true, found.value, started);
|
|
168
242
|
return found.value;
|
|
243
|
+
}
|
|
169
244
|
const value = await load();
|
|
170
|
-
found
|
|
245
|
+
found?.record(value);
|
|
246
|
+
if (this.trace)
|
|
247
|
+
this.note(fresh, false, value, started);
|
|
171
248
|
return value;
|
|
172
249
|
}
|
|
173
250
|
close() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { traceFor } from "../timing-trace.js";
|
|
1
2
|
// One owner per existing upstream request identity. Facts survive revocation;
|
|
2
3
|
// retaining them never reconstitutes a call capability or a live operation.
|
|
3
4
|
import { Context, Exit, Scope } from "effect";
|
|
@@ -71,7 +72,7 @@ export function requestReads(identity) {
|
|
|
71
72
|
export function ownerReads(owner) {
|
|
72
73
|
const retained = owner.retained;
|
|
73
74
|
if (!retained.reads) {
|
|
74
|
-
retained.reads = new RequestReads();
|
|
75
|
+
retained.reads = new RequestReads(traceFor(owner.identity));
|
|
75
76
|
if (!owner.open)
|
|
76
77
|
retained.reads.close();
|
|
77
78
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed sentence that marks downstream guidance as third-party. A prefix has
|
|
3
|
+
* no closing delimiter to forge, and connector IDs are deployment-authored.
|
|
4
|
+
*/
|
|
5
|
+
export function downstreamOrigin(connectorId) {
|
|
6
|
+
return `Third-party guidance from connector ${JSON.stringify(connectorId.slice(0, 128))}.`;
|
|
7
|
+
}
|
|
8
|
+
/** Downstream description text on one line, so it cannot begin a new listing entry. */
|
|
9
|
+
export function labeledDownstreamDescription(connectorId, description) {
|
|
10
|
+
return `${downstreamOrigin(connectorId)} ${description.replace(/\s+/g, " ").trim()}`;
|
|
11
|
+
}
|
|
12
|
+
const SKILL_RESOURCE = /^skill:\/\/|\/SKILL\.md(?:[?#]|$)/i;
|
|
13
|
+
/** Whether a downstream resource URI addresses skill instructions rather than ordinary data. */
|
|
14
|
+
export function isSkillResource(uri) {
|
|
15
|
+
return SKILL_RESOURCE.test(uri);
|
|
16
|
+
}
|