@zackbart/connecta 0.29.0 → 0.30.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 +67 -0
- package/README.md +123 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.js +3 -3
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +36 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +40 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +5 -1
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +121 -89
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +135 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +5 -0
- package/examples/worker/README.md +7 -0
- package/package.json +7 -1
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
|
+
export const skill = {
|
|
3
|
+
"content": "Start with `list_projects` for project IDs and environment slugs, then `list_folders` for absolute paths.\n\nKeep value reads narrow. `list_secrets` returns identifiers only by default, even if the server sends values or comments; use `get_secret` or opt into `includeValues` only when needed. These explicit reads return secrets, including values and comments. Comments are available only from these value-read tools. Do not echo values unless the user asks. Imports follow the same value policy.\n\nValues can reference secrets as `${env.KEY}`. Value reads expand references by default. Use `expandReferences: false` to read the literal reference before editing; updating a value changes only that secret, not the secrets it references. Updates send only named changes. Writes omit values and report `pendingApproval` when an environment's change policy requires approval.\n\nEvery value-free result returns only checked identifiers: UUID or legacy 24-hex IDs, secret keys, positive integer versions, known type/approval enums, environment and tag slugs, absolute paths, and ISO timestamps. Projects expose IDs and environment identifiers; folders expose IDs and paths. Project slugs are withheld because Infisical can derive them from free-text project names. Comments, descriptions, free-form names, reminder notes, and arbitrary nested objects are withheld. `metadataOmitted: true` reports dropped or unavailable metadata; success or pending-approval context remains available. Identifier fields outside the conservative grammar are dropped.\n\nKnown, accepted residual risk: environment slugs are returned as required routing identifiers. Infisical's UI derives them from environment names by default, so secrets entered in those names can appear in returned slugs. Operators must not put secrets in environment names or slugs. Secrets placed in identifiers or key names are out of scope.\n\nThe provider assumes an honest Infisical server and protects against careless human entry of secrets in free text. It does not match submitted values or defend against a malicious server encoding values into valid identifiers. Submitted secret data never joins credential redaction, so later calls in the same program keep their authorized arguments. Client credentials and tokens retain request-scoped credential protection.\n\nThis finite surface has no guarded raw-REST tool. These endpoints return complete collections without a page cursor; narrow the path and avoid recursive/value listings to stay below Connecta's 8 MiB response ceiling. Temporary result paging does not reduce the upstream response size.\n\nUniversal Auth tokens remain in memory only and refresh before expiry. Reads may retry once after a rejected token. Writes refresh a rejected token for a later explicit call and return the auth failure without resending; reconcile the target before another call. Recovery echoes marked `argsRedacted` omit secret values, secret comments, and folder descriptions, so any new call requires the original arguments. The operator's machine identity must belong to each project with the right role; a 403 needs an operator to fix membership or role. US cloud is the default; set `baseUrl: \"https://eu.infisical.com/api\"` for EU cloud or your self-hosted HTTPS API base including `/api`. Preserve the connector id and the `clientId`/`clientSecret` fields when migrating existing credentials.\n\nNo default call budget is assumed. Operators can supply `callAdmission` for their instance's limits. The credential test performs a Universal Auth login only, without reading projects or secrets.\n",
|
|
4
|
+
"instructionsHeading": "Organization instructions",
|
|
5
|
+
"fragments": {
|
|
6
|
+
"guide": "Start with `list_projects` for project IDs and environment slugs, then `list_folders` for absolute paths.\n\nKeep value reads narrow. `list_secrets` returns identifiers only by default, even if the server sends values or comments; use `get_secret` or opt into `includeValues` only when needed. These explicit reads return secrets, including values and comments. Comments are available only from these value-read tools. Do not echo values unless the user asks. Imports follow the same value policy.\n\nValues can reference secrets as `${env.KEY}`. Value reads expand references by default. Use `expandReferences: false` to read the literal reference before editing; updating a value changes only that secret, not the secrets it references. Updates send only named changes. Writes omit values and report `pendingApproval` when an environment's change policy requires approval.\n\nEvery value-free result returns only checked identifiers: UUID or legacy 24-hex IDs, secret keys, positive integer versions, known type/approval enums, environment and tag slugs, absolute paths, and ISO timestamps. Projects expose IDs and environment identifiers; folders expose IDs and paths. Project slugs are withheld because Infisical can derive them from free-text project names. Comments, descriptions, free-form names, reminder notes, and arbitrary nested objects are withheld. `metadataOmitted: true` reports dropped or unavailable metadata; success or pending-approval context remains available. Identifier fields outside the conservative grammar are dropped.\n\nKnown, accepted residual risk: environment slugs are returned as required routing identifiers. Infisical's UI derives them from environment names by default, so secrets entered in those names can appear in returned slugs. Operators must not put secrets in environment names or slugs. Secrets placed in identifiers or key names are out of scope.\n\nThe provider assumes an honest Infisical server and protects against careless human entry of secrets in free text. It does not match submitted values or defend against a malicious server encoding values into valid identifiers. Submitted secret data never joins credential redaction, so later calls in the same program keep their authorized arguments. Client credentials and tokens retain request-scoped credential protection.\n\nThis finite surface has no guarded raw-REST tool. These endpoints return complete collections without a page cursor; narrow the path and avoid recursive/value listings to stay below Connecta's 8 MiB response ceiling. Temporary result paging does not reduce the upstream response size.\n\nUniversal Auth tokens remain in memory only and refresh before expiry. Reads may retry once after a rejected token. Writes refresh a rejected token for a later explicit call and return the auth failure without resending; reconcile the target before another call. Recovery echoes marked `argsRedacted` omit secret values, secret comments, and folder descriptions, so any new call requires the original arguments. The operator's machine identity must belong to each project with the right role; a 403 needs an operator to fix membership or role. US cloud is the default; set `baseUrl: \"https://eu.infisical.com/api\"` for EU cloud or your self-hosted HTTPS API base including `/api`. Preserve the connector id and the `clientId`/`clientSecret` fields when migrating existing credentials.\n\nNo default call budget is assumed. Operators can supply `callAdmission` for their instance's limits. The credential test performs a Universal Auth login only, without reading projects or secrets.\n"
|
|
7
|
+
}
|
|
8
|
+
};
|
package/dist/registry.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export interface RegistryView {
|
|
|
112
112
|
resultsStorage(): KVStorage;
|
|
113
113
|
resultIdentity(): ResultIdentity;
|
|
114
114
|
/** Recheck live auth/grants and pool admission before exposing stored data. */
|
|
115
|
-
recheckResultAccess(address: string, classification: "read" | "write", signal?: AbortSignal): Promise<boolean>;
|
|
115
|
+
recheckResultAccess(address: string | undefined, classification: "read" | "write", signal?: AbortSignal, access?: "catalog"): Promise<boolean>;
|
|
116
116
|
/** Reserve deployment-wide capacity before writing a paging envelope's chunks. */
|
|
117
117
|
stashResult(id: string, chunks: readonly string[], ttlSeconds: number): Promise<boolean>;
|
|
118
118
|
/** Local declared-vs-stored credential mismatch, with no downstream I/O. */
|
|
@@ -142,7 +142,7 @@ export interface RegistryScope {
|
|
|
142
142
|
principalKey?: string;
|
|
143
143
|
endpoint?: string;
|
|
144
144
|
origin?: string | null;
|
|
145
|
-
currentResultAccess?: (address: string, classification: "read" | "write", signal?: AbortSignal) => Promise<boolean>;
|
|
145
|
+
currentResultAccess?: (address: string | undefined, classification: "read" | "write", signal?: AbortSignal, access?: "catalog") => Promise<boolean>;
|
|
146
146
|
/** The admitted caller, readable only by built-in connectors; see connector-caller.ts. */
|
|
147
147
|
caller?: ConnectorCaller;
|
|
148
148
|
}
|
|
@@ -241,9 +241,11 @@ export declare class Registry implements RegistryView {
|
|
|
241
241
|
* The bounds are the deployment's, not this isolate's. Every charge is a
|
|
242
242
|
* row in one ledger record in storage, booked by compare-and-set before any
|
|
243
243
|
* chunk is written, so every isolate and process sharing the store sees the
|
|
244
|
-
* same entries and bytes. A
|
|
245
|
-
*
|
|
246
|
-
*
|
|
244
|
+
* same entries and bytes. A pending write keeps its finite charge reserved.
|
|
245
|
+
* Writes stop after 30 seconds; absolute chunk expiries cannot pass the
|
|
246
|
+
* booked deadline even on late commit. Rejected responses leave writes
|
|
247
|
+
* unconfirmed and retain their charge. Cleanup/release and settlement each
|
|
248
|
+
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
247
249
|
*/
|
|
248
250
|
stashResult(id: string, chunks: readonly string[], ttlSeconds: number, partition?: string): Promise<boolean>;
|
|
249
251
|
/**
|
|
@@ -251,7 +253,7 @@ export declare class Registry implements RegistryView {
|
|
|
251
253
|
* connector's namespace. Backs connecta.result.
|
|
252
254
|
*/
|
|
253
255
|
resultIdentity(): ResultIdentity;
|
|
254
|
-
recheckResultAccess(_address: string, _classification: "read" | "write", _signal?: AbortSignal): Promise<boolean>;
|
|
256
|
+
recheckResultAccess(_address: string | undefined, _classification: "read" | "write", _signal?: AbortSignal, _access?: "catalog"): Promise<boolean>;
|
|
255
257
|
resultsStorage(): KVStorage;
|
|
256
258
|
credentialUiAvailable(): boolean;
|
|
257
259
|
bindOAuthHandoff(id: string, authorizationUrl: string, principalKey?: string): Promise<void>;
|
package/dist/registry.js
CHANGED
|
@@ -19,7 +19,7 @@ import { attachCaller } from "./connector-caller.js";
|
|
|
19
19
|
import { runEdge } from "./runtime/run.js";
|
|
20
20
|
import { SharedRead } from "./runtime/shared-read.js";
|
|
21
21
|
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet } from "./runtime/storage.js";
|
|
22
|
-
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, } from "./storage/keys.js";
|
|
22
|
+
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, stashExpiryKeys, } from "./storage/keys.js";
|
|
23
23
|
import { classifyTool } from "./tool-safety.js";
|
|
24
24
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
25
25
|
const DEFAULT_MAX_RESULT_BYTES = CONFIG_DEFAULTS.calls.maxResultBytes;
|
|
@@ -87,6 +87,7 @@ function frozenFacts(tools) {
|
|
|
87
87
|
}
|
|
88
88
|
function namespaced(storage, prefix) {
|
|
89
89
|
return {
|
|
90
|
+
capabilities: storage.capabilities,
|
|
90
91
|
get: (k) => storage.get(prefix + k),
|
|
91
92
|
set: (k, v, o) => storage.set(prefix + k, v, o),
|
|
92
93
|
delete: (k) => storage.delete(prefix + k),
|
|
@@ -108,19 +109,21 @@ const STASH_LEDGER_ATTEMPTS = 32;
|
|
|
108
109
|
/** First backoff window after a lost swap; it doubles up to the cap. */
|
|
109
110
|
const STASH_LEDGER_BACKOFF_MS = 4;
|
|
110
111
|
const STASH_LEDGER_BACKOFF_CAP_MS = 250;
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
/** Whole chunk-write phase, measured from booking, including the header. */
|
|
113
|
+
const STASH_WRITE_TIMEOUT_MS = 30_000;
|
|
114
|
+
/** Total cleanup/release or settlement budget, including ledger retries. */
|
|
115
|
+
const STASH_COMPLETION_TIMEOUT_MS = 15_000;
|
|
116
|
+
/** Stop waiting without starting background storage work after the deadline. */
|
|
117
|
+
function completeStash(effect) {
|
|
118
|
+
return effect.pipe(Effect.asVoid, Effect.timeoutOrElse({ duration: Duration.millis(STASH_COMPLETION_TIMEOUT_MS), orElse: () => Effect.void }),
|
|
119
|
+
// onError finalizers are otherwise uninterruptible, including their I/O.
|
|
120
|
+
Effect.interruptible);
|
|
121
|
+
}
|
|
119
122
|
/**
|
|
120
123
|
* Live charges in a stored ledger. Anything malformed reads as empty: the
|
|
121
124
|
* ledger bounds an advisory cache, and the next successful swap replaces it.
|
|
122
125
|
*/
|
|
123
|
-
function
|
|
126
|
+
function stashCharges(raw) {
|
|
124
127
|
if (raw === null)
|
|
125
128
|
return [];
|
|
126
129
|
let parsed;
|
|
@@ -138,16 +141,36 @@ function liveStashCharges(raw, now) {
|
|
|
138
141
|
typeof entry[0] === "string" &&
|
|
139
142
|
Number.isSafeInteger(entry[1]) &&
|
|
140
143
|
entry[1] >= 0 &&
|
|
141
|
-
|
|
142
|
-
entry[2] > now);
|
|
144
|
+
Number.isSafeInteger(entry[2]));
|
|
143
145
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
/** Caller time can nominate a charge for pruning, but only storage proves expiry. */
|
|
147
|
+
function liveStashCharges(raw, now) {
|
|
148
|
+
return Effect.gen(function* () {
|
|
149
|
+
const live = [];
|
|
150
|
+
const expired = new Map();
|
|
151
|
+
for (const entry of stashCharges(raw)) {
|
|
152
|
+
const deadline = entry[2];
|
|
153
|
+
if (deadline > now) {
|
|
154
|
+
live.push(entry);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let proven = expired.get(deadline);
|
|
158
|
+
if (proven === undefined) {
|
|
159
|
+
// A successful absolute-expiry write followed by an absent read proves
|
|
160
|
+
// storage has passed this deadline, even when the caller runs ahead.
|
|
161
|
+
// These probes expire at the deadline they check; no cleanup is needed.
|
|
162
|
+
const key = stashExpiryKeys.deadline(deadline);
|
|
163
|
+
yield* storageSet(key, "live", { expiresAtMs: deadline });
|
|
164
|
+
proven = (yield* storageGet(key)) === null;
|
|
165
|
+
expired.set(deadline, proven);
|
|
166
|
+
}
|
|
167
|
+
if (!proven)
|
|
168
|
+
live.push(entry);
|
|
169
|
+
}
|
|
170
|
+
return live;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Rewrite the ledger with bounded retries; every stored charge already expires. */
|
|
151
174
|
function swapStashLedger(plan) {
|
|
152
175
|
return Effect.gen(function* () {
|
|
153
176
|
const ledger = stashLedgerKeys.ledger;
|
|
@@ -156,16 +179,34 @@ function swapStashLedger(plan) {
|
|
|
156
179
|
const window = Math.min(STASH_LEDGER_BACKOFF_CAP_MS, STASH_LEDGER_BACKOFF_MS * 2 ** (attempt - 1));
|
|
157
180
|
yield* Effect.sleep(Duration.millis(Math.floor((yield* Random.next) * window) + 1));
|
|
158
181
|
}
|
|
182
|
+
const outcome = yield* Effect.gen(function* () {
|
|
183
|
+
const raw = yield* storageGet(ledger);
|
|
184
|
+
const now = yield* Clock.currentTimeMillis;
|
|
185
|
+
const planned = plan(yield* liveStashCharges(raw, now), now);
|
|
186
|
+
if (planned.entries !== undefined) {
|
|
187
|
+
const entries = planned.entries;
|
|
188
|
+
// The ledger row itself expires with its last charge. A delayed CAS
|
|
189
|
+
// cannot create an immortal row or restart any reservation's lifetime.
|
|
190
|
+
const next = entries.length ? jsonCodec.encode({ v: 1, entries }) : null;
|
|
191
|
+
const expiresAtMs = entries.reduce((latest, entry) => Math.max(latest, entry[2]), entries[0]?.[2] ?? now);
|
|
192
|
+
if (!(yield* storageCompareAndSet(ledger, raw, next, { expiresAtMs })))
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return { status: "applied", result: planned.result };
|
|
196
|
+
}).pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
197
|
+
if (outcome !== undefined)
|
|
198
|
+
return outcome;
|
|
199
|
+
}
|
|
200
|
+
// The final CAS can commit and then throw too. Confirm a read-only plan
|
|
201
|
+
// once more, especially an already-booked reservation, without another CAS.
|
|
202
|
+
return yield* Effect.gen(function* () {
|
|
159
203
|
const raw = yield* storageGet(ledger);
|
|
160
204
|
const now = yield* Clock.currentTimeMillis;
|
|
161
|
-
const planned = plan(liveStashCharges(raw, now), now);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
205
|
+
const planned = plan(yield* liveStashCharges(raw, now), now);
|
|
206
|
+
return planned.entries === undefined
|
|
207
|
+
? { status: "applied", result: planned.result }
|
|
208
|
+
: { status: "exhausted" };
|
|
209
|
+
}).pipe(Effect.catch(() => Effect.succeed({ status: "exhausted" })));
|
|
169
210
|
});
|
|
170
211
|
}
|
|
171
212
|
async function sha256Hex(value) {
|
|
@@ -551,9 +592,11 @@ export class Registry {
|
|
|
551
592
|
* The bounds are the deployment's, not this isolate's. Every charge is a
|
|
552
593
|
* row in one ledger record in storage, booked by compare-and-set before any
|
|
553
594
|
* chunk is written, so every isolate and process sharing the store sees the
|
|
554
|
-
* same entries and bytes. A
|
|
555
|
-
*
|
|
556
|
-
*
|
|
595
|
+
* same entries and bytes. A pending write keeps its finite charge reserved.
|
|
596
|
+
* Writes stop after 30 seconds; absolute chunk expiries cannot pass the
|
|
597
|
+
* booked deadline even on late commit. Rejected responses leave writes
|
|
598
|
+
* unconfirmed and retain their charge. Cleanup/release and settlement each
|
|
599
|
+
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
557
600
|
*/
|
|
558
601
|
stashResult(id, chunks, ttlSeconds, partition = scopes.results) {
|
|
559
602
|
return runOnPartition(Effect.gen({ self: this }, function* () {
|
|
@@ -564,48 +607,78 @@ export class Registry {
|
|
|
564
607
|
if (bytes > maxBytes || maxEntries === 0)
|
|
565
608
|
return false;
|
|
566
609
|
const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
610
|
+
// IDs belong to reservations, not reusable result IDs or subjects.
|
|
611
|
+
const charge = crypto.randomUUID();
|
|
612
|
+
const ttlMs = ttlSeconds * 1000;
|
|
613
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
614
|
+
return false;
|
|
615
|
+
const booking = yield* swapStashLedger((live, now) => {
|
|
616
|
+
// A committed CAS with a lost response is already our booking, even
|
|
617
|
+
// at full capacity. Reuse its deadline instead of adding a second slot.
|
|
618
|
+
const own = live.find((entry) => entry[0] === charge);
|
|
619
|
+
if (own)
|
|
620
|
+
return { result: own[2] };
|
|
573
621
|
const used = live.reduce((sum, entry) => sum + entry[1], 0);
|
|
574
622
|
if (live.length >= maxEntries || used + bytes > maxBytes)
|
|
575
623
|
return { result: undefined };
|
|
576
|
-
const
|
|
577
|
-
|
|
624
|
+
const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
|
|
625
|
+
if (!Number.isSafeInteger(deadline))
|
|
626
|
+
return { result: undefined };
|
|
627
|
+
return { entries: [...live, [charge, bytes, deadline]], result: deadline };
|
|
578
628
|
});
|
|
579
|
-
if (
|
|
629
|
+
if (booking.status === "exhausted" || booking.result === undefined)
|
|
580
630
|
return false;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
// a write
|
|
631
|
+
const deadline = booking.result;
|
|
632
|
+
const writeDeadline = deadline - ttlMs;
|
|
633
|
+
const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
|
|
634
|
+
const owns = (entry) => entry[0] === charge;
|
|
635
|
+
const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
|
|
636
|
+
? {
|
|
637
|
+
entries: live.map((entry) => (owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry)),
|
|
638
|
+
result: undefined,
|
|
639
|
+
}
|
|
640
|
+
: { result: undefined });
|
|
641
|
+
let pendingWrite = false;
|
|
642
|
+
// A rejected or timed-out driver call may still commit after deletion.
|
|
643
|
+
// Only a confirmed write response clears pendingWrite.
|
|
644
|
+
const release = completeStash(Effect.gen(function* () {
|
|
645
|
+
const deleted = yield* Effect.gen(function* () {
|
|
646
|
+
for (const key of keys)
|
|
647
|
+
yield* storageDelete(key);
|
|
648
|
+
return true;
|
|
649
|
+
}).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
650
|
+
if (pendingWrite)
|
|
651
|
+
return;
|
|
652
|
+
if (deleted) {
|
|
653
|
+
const released = yield* swapStashLedger((live) => live.some(owns)
|
|
654
|
+
? { entries: live.filter((entry) => !owns(entry)), result: undefined }
|
|
655
|
+
: { result: undefined });
|
|
656
|
+
if (released.status === "applied")
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
yield* settle;
|
|
660
|
+
}));
|
|
661
|
+
// Trailing chunks first, then the header. The caller receives no result
|
|
662
|
+
// ID after failure or timeout, including a late header commit.
|
|
663
|
+
const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
|
|
594
664
|
const written = yield* Effect.gen(function* () {
|
|
595
665
|
for (let index = keys.length - 1; index >= 0; index--) {
|
|
596
666
|
const before = yield* Clock.currentTimeMillis;
|
|
597
|
-
|
|
598
|
-
// Zero would mean no expiry: a stash that outlasts its deadline fails.
|
|
599
|
-
if (remaining < 1)
|
|
667
|
+
if (before >= writeDeadline || before >= chunkExpiry)
|
|
600
668
|
return false;
|
|
601
|
-
|
|
602
|
-
|
|
669
|
+
pendingWrite = true;
|
|
670
|
+
yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
671
|
+
pendingWrite = false;
|
|
672
|
+
})));
|
|
673
|
+
if ((yield* Clock.currentTimeMillis) >= writeDeadline)
|
|
603
674
|
return false;
|
|
604
675
|
}
|
|
605
676
|
return true;
|
|
606
|
-
}).pipe(Effect.onError(() => release));
|
|
677
|
+
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
|
|
607
678
|
if (!written)
|
|
608
679
|
yield* release;
|
|
680
|
+
else
|
|
681
|
+
yield* completeStash(settle);
|
|
609
682
|
return written;
|
|
610
683
|
}), this.opts);
|
|
611
684
|
}
|
|
@@ -616,7 +689,7 @@ export class Registry {
|
|
|
616
689
|
resultIdentity() {
|
|
617
690
|
return { subject: null, principal: null, endpoint: "/mcp", origin: null };
|
|
618
691
|
}
|
|
619
|
-
recheckResultAccess(_address, _classification, _signal) {
|
|
692
|
+
recheckResultAccess(_address, _classification, _signal, _access) {
|
|
620
693
|
return Promise.resolve(true);
|
|
621
694
|
}
|
|
622
695
|
resultsStorage() {
|
|
@@ -996,8 +1069,8 @@ class ScopedRegistryView {
|
|
|
996
1069
|
origin: this.scope.origin ?? null,
|
|
997
1070
|
};
|
|
998
1071
|
}
|
|
999
|
-
recheckResultAccess(address, classification, signal) {
|
|
1000
|
-
return this.scope.currentResultAccess?.(address, classification, signal) ?? Promise.resolve(true);
|
|
1072
|
+
recheckResultAccess(address, classification, signal, access) {
|
|
1073
|
+
return this.scope.currentResultAccess?.(address, classification, signal, access) ?? Promise.resolve(true);
|
|
1001
1074
|
}
|
|
1002
1075
|
resultsStorage() {
|
|
1003
1076
|
return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
|
package/dist/resource-read.js
CHANGED
|
@@ -39,6 +39,7 @@ export function readResource(registry, raw, options) {
|
|
|
39
39
|
return withDeadlineEffect((signal) => Effect.scoped(Effect.gen(function* () {
|
|
40
40
|
const target = yield* Effect.try({ try: () => resourceTarget(registry, raw), catch: (error) => error });
|
|
41
41
|
connector = target.connector;
|
|
42
|
+
options.onResolved?.(target.connector);
|
|
42
43
|
// Await this Promise in the reader's request context, as invocation.ts does.
|
|
43
44
|
yield* Effect.acquireRelease(Effect.suspend(() => {
|
|
44
45
|
const pending = registry.admitCall(target.connector.id, {
|
|
@@ -50,7 +51,8 @@ export function readResource(registry, raw, options) {
|
|
|
50
51
|
pending.then((permit) => permit.release(), () => { });
|
|
51
52
|
})));
|
|
52
53
|
}), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
|
|
53
|
-
|
|
54
|
+
// Downstream contents enter the guest here, so they take the full payload rules.
|
|
55
|
+
const contents = yield* timed((elapsed) => options.onConnectorTime?.(elapsed), Effect.tryPromise({
|
|
54
56
|
try: () => {
|
|
55
57
|
if (signal.aborted)
|
|
56
58
|
throw signal.reason;
|
|
@@ -66,6 +68,7 @@ export function readResource(registry, raw, options) {
|
|
|
66
68
|
},
|
|
67
69
|
catch: (error) => error,
|
|
68
70
|
}));
|
|
71
|
+
return options.sentSecrets.redact(contents);
|
|
69
72
|
})), {
|
|
70
73
|
timeoutMs: options.timeoutMs,
|
|
71
74
|
...(options.signal ? { signal: options.signal } : {}),
|
package/dist/routes/mcp.js
CHANGED
|
@@ -675,7 +675,7 @@ export function createMcpRoute(opts) {
|
|
|
675
675
|
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
676
676
|
endpoint: new URL(request.url).pathname,
|
|
677
677
|
origin: request.headers.get("Origin"),
|
|
678
|
-
currentResultAccess: async (address, classification, signal) => {
|
|
678
|
+
currentResultAccess: async (address, classification, signal, access) => {
|
|
679
679
|
signal?.throwIfAborted();
|
|
680
680
|
const current = await authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity);
|
|
681
681
|
if (!current.ok) {
|
|
@@ -699,6 +699,13 @@ export function createMcpRoute(opts) {
|
|
|
699
699
|
...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
|
|
700
700
|
...(current.principalKey ? { principalKey: current.principalKey } : {}),
|
|
701
701
|
});
|
|
702
|
+
if (classification === "write" && currentTrust !== "trusted")
|
|
703
|
+
return false;
|
|
704
|
+
if (address === undefined)
|
|
705
|
+
return true;
|
|
706
|
+
// Skill and resource content requires a whole grant; catalog metadata requires visibility.
|
|
707
|
+
if (!address.includes("."))
|
|
708
|
+
return Boolean(access === "catalog" ? view.getConnector(address) : view.getResourceConnector(address));
|
|
702
709
|
const resolved = view.resolveAddress(address);
|
|
703
710
|
if (!resolved)
|
|
704
711
|
return false;
|
|
@@ -707,7 +714,9 @@ export function createMcpRoute(opts) {
|
|
|
707
714
|
const tool = (await view.getTools(resolved.connector.id, baseUrl, requestScope, signal ? { signal } : {})).find((tool) => tool.name === resolved.toolName);
|
|
708
715
|
signal?.throwIfAborted();
|
|
709
716
|
return Boolean(tool &&
|
|
710
|
-
(
|
|
717
|
+
(access === "catalog" ||
|
|
718
|
+
(classification === "read" && tool.classification === "read") ||
|
|
719
|
+
currentTrust === "trusted"));
|
|
711
720
|
}
|
|
712
721
|
finally {
|
|
713
722
|
await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
|
package/dist/sent-secrets.d.ts
CHANGED
|
@@ -1,30 +1,69 @@
|
|
|
1
|
-
import type { ConnectorContext, Logger } from "./types.js";
|
|
1
|
+
import type { ConnectorContext, JsonSchema, Logger } from "./types.js";
|
|
2
2
|
/** One payload-free warning per configured connector, regardless of requests. */
|
|
3
3
|
export declare function shortSecretWarning(): (value: string | undefined, logger: Logger) => void;
|
|
4
4
|
export declare class SentSecrets {
|
|
5
|
-
|
|
6
|
-
private
|
|
7
|
-
private unicode;
|
|
5
|
+
/** Normalized form to its strongest kind. */
|
|
6
|
+
private readonly forms;
|
|
8
7
|
private readonly recipients;
|
|
9
|
-
private
|
|
10
|
-
/** The
|
|
8
|
+
private privateChars;
|
|
9
|
+
/** The shortest form left unregistered; any string this long might contain it. */
|
|
10
|
+
private unmatched;
|
|
11
|
+
private unicode;
|
|
12
|
+
private views;
|
|
13
|
+
private store;
|
|
14
|
+
private unregistered;
|
|
15
|
+
private register;
|
|
16
|
+
/** The request receives existing and future secrets from every context. */
|
|
11
17
|
include(source: SentSecrets): void;
|
|
12
18
|
add(value: string): void;
|
|
13
19
|
/** Explicit secrets use the same floor as every other credential. */
|
|
14
20
|
secret(value: string): void;
|
|
21
|
+
/** Submitted writeOnly strings and numbers join this request's set, including property
|
|
22
|
+
* names inside a private object. Empty strings, booleans, and null carry no text. */
|
|
23
|
+
arguments(args: unknown, schema: JsonSchema | undefined): void;
|
|
15
24
|
/** Structural fields must be refused, never repaired into different URLs. */
|
|
16
25
|
contains(value: string, ignoreCase?: boolean): boolean;
|
|
17
26
|
containsUrl(value: string): boolean;
|
|
18
27
|
header(value: string): void;
|
|
19
28
|
request(input: RequestInfo | URL, init?: RequestInit): void;
|
|
29
|
+
/** Serialized wire text about to be returned, stored, or paged, under payload rules. */
|
|
20
30
|
text(value: string): string;
|
|
21
|
-
/**
|
|
31
|
+
/** Copy only what changes, including non-enumerable Error fields; never retain a raw cause.
|
|
32
|
+
* Payload rules apply where downstream or guest data enters; envelopes repeat literal
|
|
33
|
+
* replacement. */
|
|
34
|
+
redact<T>(value: T, mode?: "payload" | "prose" | "envelope"): T;
|
|
35
|
+
/** Credentials alone: later calls keep the private arguments they submit, and cached
|
|
36
|
+
* catalogs never depend on one request's arguments. */
|
|
37
|
+
redactCredentials<T>(value: T): T;
|
|
38
|
+
private idle;
|
|
39
|
+
private scan;
|
|
40
|
+
private matcher;
|
|
41
|
+
private credentials;
|
|
42
|
+
/** Strings at least this long might hold a form that was never matched. */
|
|
43
|
+
private floor;
|
|
44
|
+
/** Every raw and JSON-unescaped match, mapped to source spans before any replacement. Text
|
|
45
|
+
* decodes one more layer while the last one could still hold an escape, up to
|
|
46
|
+
* MAX_DECODE_LEVELS counting the JSON strings it came from. Work beyond the call's budget
|
|
47
|
+
* or that depth returns undefined, which withholds the string. */
|
|
48
|
+
private find;
|
|
49
|
+
/** A structured leaf, property name, or prose. A short private form withholds prose
|
|
50
|
+
* wherever it occurs, and a leaf only when the whole leaf is that form. */
|
|
51
|
+
private string;
|
|
52
|
+
/** A number, boolean, or null matching by its canonical text becomes a placeholder string. */
|
|
53
|
+
private scalar;
|
|
54
|
+
/** JSON objects, arrays, and string literals are parsed and walked, nested JSON strings
|
|
55
|
+
* included; their serialized bytes must then hold no literal the walk cannot see, such as
|
|
56
|
+
* one spanning tokens. Other text is prose or a structured leaf. */
|
|
57
|
+
private wire;
|
|
58
|
+
/** Iterative and copy-on-write: only containers on a changed path are copied. A container's
|
|
59
|
+
* entries are counted against the node budget before it is walked; a cycle or a container
|
|
60
|
+
* past MAX_WALK_DEPTH is withheld, and one past the node budget is checked as serialized text. */
|
|
61
|
+
private walk;
|
|
62
|
+
/** A container larger than the node budget is scanned as serialized text, Error fields
|
|
63
|
+
* included: kept unchanged when nothing registered appears, otherwise withheld whole. */
|
|
64
|
+
private serialized;
|
|
65
|
+
/** Skill supporting files may contain arbitrary bytes, including echoes. */
|
|
22
66
|
private blob;
|
|
23
|
-
/** Decode bounded windows with source spans, including twice-escaped JSON.
|
|
24
|
-
* Scratch space follows credential length, never the whole skill file. */
|
|
25
|
-
private escapedText;
|
|
26
|
-
/** Copy, including non-enumerable Error fields; never retain a raw cause. */
|
|
27
|
-
redact<T>(value: T): T;
|
|
28
67
|
/** Match across blocks before isError/JSON unwrapping concatenates them. */
|
|
29
68
|
private joinedContent;
|
|
30
69
|
}
|
|
@@ -33,7 +72,9 @@ export declare function sentSecretsFor(ctx: ConnectorContext): SentSecrets;
|
|
|
33
72
|
export declare function sentSecretsForRequest(scope: object, secrets?: SentSecrets): SentSecrets;
|
|
34
73
|
/** The agent-facing choke point. Apply to values before bridge serialization
|
|
35
74
|
* and to serialized wire text, so JSON escapes and structured strings share
|
|
36
|
-
* the same rule.
|
|
75
|
+
* the same rule. Payloads were redacted where they entered; this boundary
|
|
76
|
+
* repeats credential and literal private-value replacement around Connecta's
|
|
77
|
+
* own envelopes, whose identifiers and counts are never private data. */
|
|
37
78
|
export declare function redactAgentOutput<T>(secrets: SentSecrets, value: T): T;
|
|
38
79
|
/** Wrap the complete operation table, including rejections. Adding an operation
|
|
39
80
|
* cannot add an unredacted exit. A direct invocation owns a fresh scope unless
|