@zackbart/connecta 0.28.0 → 0.29.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 +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ActivityStore } from "../activity.js";
|
|
2
|
+
import type { KVStorage } from "../types.js";
|
|
3
|
+
type SqlValue = string | number | null;
|
|
4
|
+
export interface SqlStatement {
|
|
5
|
+
readonly sql: string;
|
|
6
|
+
readonly params: readonly SqlValue[];
|
|
7
|
+
}
|
|
8
|
+
/** What a store needs from a SQLite-dialect database. */
|
|
9
|
+
export interface SqlDriver {
|
|
10
|
+
/** Rows a query returns. */
|
|
11
|
+
all<Row>(statement: SqlStatement): Promise<Row[]>;
|
|
12
|
+
/** Run one write; resolve with the number of rows it changed. */
|
|
13
|
+
run(statement: SqlStatement): Promise<number>;
|
|
14
|
+
/**
|
|
15
|
+
* Run writes in order as one transaction; resolve with the rows each
|
|
16
|
+
* statement changed.
|
|
17
|
+
*/
|
|
18
|
+
batch(statements: readonly SqlStatement[]): Promise<number[]>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The key-value table. Its shape is the one the 0.28 Worker example's
|
|
22
|
+
* `d1-storage.ts` documented, so a D1 database that already holds it is read
|
|
23
|
+
* as is.
|
|
24
|
+
*/
|
|
25
|
+
export declare const KV_SCHEMA: readonly string[];
|
|
26
|
+
/** The two drivers, as `describe()` names them. */
|
|
27
|
+
export type SqlKind = "d1" | "sqlite";
|
|
28
|
+
/** `KVStorage` over one `connecta_kv` table. */
|
|
29
|
+
export declare function sqlStorage(driver: SqlDriver, kind: SqlKind): KVStorage;
|
|
30
|
+
/** One entry copied from another store, with its absolute expiry. */
|
|
31
|
+
export interface SqlCopyEntry {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
readonly value: string;
|
|
34
|
+
/** Epoch milliseconds; null never expires. */
|
|
35
|
+
readonly expiresAtMs: number | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What a copy did with one entry: written where no live row was, left alone
|
|
39
|
+
* because the live row already held the value, refused because it held
|
|
40
|
+
* another (`conflict`), or replaced under `overwrite` (`overwritten`).
|
|
41
|
+
*/
|
|
42
|
+
export type SqlCopyOutcome = "copied" | "unchanged" | "conflict" | "overwritten";
|
|
43
|
+
/**
|
|
44
|
+
* Copy entries into the key-value table, verbatim: same key, same value,
|
|
45
|
+
* same absolute expiry. A live row holding a different value is kept unless
|
|
46
|
+
* `overwrite` is set, so a second copy of the same entries writes nothing.
|
|
47
|
+
* Writes go out in bounded transactions; the outcomes follow `entries`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function copyIntoSql(driver: SqlDriver, entries: readonly SqlCopyEntry[], options: {
|
|
50
|
+
overwrite: boolean;
|
|
51
|
+
now: number;
|
|
52
|
+
}): Promise<SqlCopyOutcome[]>;
|
|
53
|
+
export interface SqlActivityOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Days of history kept. Each write removes a bounded batch of older rows,
|
|
56
|
+
* so the table stays near this window without a scheduled job. Default 90.
|
|
57
|
+
*/
|
|
58
|
+
retentionDays?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One append-only row per completed downstream call. Rows carry no arguments,
|
|
62
|
+
* results, generated code, or raw error messages — the store never has a
|
|
63
|
+
* payload to leak.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sqlActivityStore(driver: SqlDriver, kind: SqlKind, options?: SqlActivityOptions): ActivityStore;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// One SQL key-value store and one SQL activity store, over two drivers.
|
|
2
|
+
//
|
|
3
|
+
// D1 (`src/d1.ts`) and Node's built-in SQLite (`src/sqlite.ts`) speak the same
|
|
4
|
+
// SQLite dialect, so every statement here runs unchanged on both. A driver is
|
|
5
|
+
// three methods; nothing in this module knows which one it has. It imports no
|
|
6
|
+
// platform module, so the Worker subpath can carry it.
|
|
7
|
+
//
|
|
8
|
+
// Atomicity. Every compare-and-set is one statement. SQLite executes a
|
|
9
|
+
// statement atomically against the database file, and D1 runs one database's
|
|
10
|
+
// statements one at a time against its primary, so a comparison and its write
|
|
11
|
+
// cannot interleave with another request's: of fifty concurrent claims on an
|
|
12
|
+
// absent key, exactly one changes a row.
|
|
13
|
+
//
|
|
14
|
+
// The database clock creates and checks KV expiry inside each statement.
|
|
15
|
+
// Isolate clock skew cannot shorten another holder's TTL. An expired row reads
|
|
16
|
+
// as absent at once and is removed
|
|
17
|
+
// physically by later writes, a bounded batch at a time, so no request starts
|
|
18
|
+
// a background job and no cron is required.
|
|
19
|
+
//
|
|
20
|
+
// Schema. Each store creates its own tables and indexes the first time one of
|
|
21
|
+
// its methods runs in an isolate or process, with idempotent statements, so a
|
|
22
|
+
// deployment applies no migration by hand. The flag that records it is a
|
|
23
|
+
// boolean, never a shared promise: a request that sees it unset runs the
|
|
24
|
+
// statements itself (INV-7).
|
|
25
|
+
import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
|
|
26
|
+
import { assertKnownOptions, ConfigError, keys, optionsOf } from "../config-schema.js";
|
|
27
|
+
import { agentFrictionForCode } from "../activity-friction.js";
|
|
28
|
+
import { validateStorageKey } from "./keys.js";
|
|
29
|
+
const sql = (text, ...params) => ({
|
|
30
|
+
sql: text,
|
|
31
|
+
params,
|
|
32
|
+
});
|
|
33
|
+
/** Select `column`, and beside it `<column>_bytes` when it holds a NUL. */
|
|
34
|
+
const textColumn = (column) => `${column}, CASE WHEN instr(CAST(${column} AS BLOB), x'00') > 0
|
|
35
|
+
THEN CAST(${column} AS BLOB) END AS ${column}_bytes`;
|
|
36
|
+
// Everything written here was a JavaScript string, so the bytes are UTF-8;
|
|
37
|
+
// a stored row that is not is refused rather than read wrong. A leading
|
|
38
|
+
// U+FEFF is part of the value, not a byte order mark.
|
|
39
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
40
|
+
/** A text column's value, from its bytes when the read selected them. */
|
|
41
|
+
function textOf(text, bytes, what) {
|
|
42
|
+
if (bytes == null)
|
|
43
|
+
return text;
|
|
44
|
+
try {
|
|
45
|
+
return utf8.decode(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new TypeError(`${what} is not valid UTF-8`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Run `ddl` once per store object; a failure leaves it to the next call. */
|
|
52
|
+
function schemaOnce(driver, ddl) {
|
|
53
|
+
let ready = false;
|
|
54
|
+
return async () => {
|
|
55
|
+
if (ready)
|
|
56
|
+
return;
|
|
57
|
+
await ddl(driver);
|
|
58
|
+
ready = true;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// --- key-value ----------------------------------------------------------
|
|
62
|
+
/**
|
|
63
|
+
* The key-value table. Its shape is the one the 0.28 Worker example's
|
|
64
|
+
* `d1-storage.ts` documented, so a D1 database that already holds it is read
|
|
65
|
+
* as is.
|
|
66
|
+
*/
|
|
67
|
+
export const KV_SCHEMA = [
|
|
68
|
+
`CREATE TABLE IF NOT EXISTS connecta_kv (
|
|
69
|
+
key TEXT PRIMARY KEY,
|
|
70
|
+
value TEXT NOT NULL,
|
|
71
|
+
expires_at_ms INTEGER
|
|
72
|
+
)`,
|
|
73
|
+
`CREATE INDEX IF NOT EXISTS connecta_kv_expiry
|
|
74
|
+
ON connecta_kv (expires_at_ms)`,
|
|
75
|
+
];
|
|
76
|
+
/** Expired rows one write removes on its way. */
|
|
77
|
+
const SWEEP_ROWS = 16;
|
|
78
|
+
function ttlMillis(ttlSeconds) {
|
|
79
|
+
if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
|
|
80
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
81
|
+
}
|
|
82
|
+
if (!ttlSeconds)
|
|
83
|
+
return null;
|
|
84
|
+
const expiry = ttlSeconds * 1000;
|
|
85
|
+
if (!Number.isFinite(expiry)) {
|
|
86
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
87
|
+
}
|
|
88
|
+
return Math.ceil(expiry);
|
|
89
|
+
}
|
|
90
|
+
/** `KVStorage` over one `connecta_kv` table. */
|
|
91
|
+
export function sqlStorage(driver, kind) {
|
|
92
|
+
const ensure = schemaOnce(driver, async (d) => {
|
|
93
|
+
await d.batch(KV_SCHEMA.map((statement) => sql(statement)));
|
|
94
|
+
});
|
|
95
|
+
// SQLite and D1 evaluate 'now' in the database, never in a caller isolate.
|
|
96
|
+
const now = "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
97
|
+
const live = `(expires_at_ms IS NULL OR expires_at_ms > ${now})`;
|
|
98
|
+
const expiryValue = `(${now} + ?)`;
|
|
99
|
+
const current = async (key) => {
|
|
100
|
+
const [row] = await driver.all(sql(`SELECT ${textColumn("value")} FROM connecta_kv WHERE key = ? AND ${live}`, key));
|
|
101
|
+
return row ? textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(key)}`) : null;
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
describe: () => ({ kind }),
|
|
105
|
+
async get(key) {
|
|
106
|
+
validateStorageKey(key);
|
|
107
|
+
await ensure();
|
|
108
|
+
return current(key);
|
|
109
|
+
},
|
|
110
|
+
async set(key, value, opts) {
|
|
111
|
+
validateStorageKey(key);
|
|
112
|
+
await ensure();
|
|
113
|
+
const expiry = ttlMillis(opts?.ttlSeconds);
|
|
114
|
+
await driver.batch([
|
|
115
|
+
sql(`DELETE FROM connecta_kv WHERE key IN (
|
|
116
|
+
SELECT key FROM connecta_kv WHERE expires_at_ms <= ${now} LIMIT ${SWEEP_ROWS}
|
|
117
|
+
)`),
|
|
118
|
+
sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
119
|
+
VALUES (?, ?, ${expiryValue})
|
|
120
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
121
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry),
|
|
122
|
+
]);
|
|
123
|
+
},
|
|
124
|
+
async delete(key) {
|
|
125
|
+
validateStorageKey(key);
|
|
126
|
+
await ensure();
|
|
127
|
+
await driver.run(sql("DELETE FROM connecta_kv WHERE key = ?", key));
|
|
128
|
+
},
|
|
129
|
+
async list(prefix) {
|
|
130
|
+
validateStorageKey(prefix);
|
|
131
|
+
await ensure();
|
|
132
|
+
const rows = await driver.all(sql(`SELECT key FROM connecta_kv
|
|
133
|
+
WHERE key >= ?
|
|
134
|
+
AND substr(CAST(key AS BLOB), 1, length(CAST(? AS BLOB))) = CAST(? AS BLOB)
|
|
135
|
+
AND ${live}`, prefix, prefix, prefix));
|
|
136
|
+
// Sort in JavaScript: SQLite orders UTF-8 bytes, the contract orders
|
|
137
|
+
// the UTF-16 code units every other adapter sorts by.
|
|
138
|
+
return rows.map((row) => row.key).sort();
|
|
139
|
+
},
|
|
140
|
+
async compareAndSet(key, expected, next, opts) {
|
|
141
|
+
validateStorageKey(key);
|
|
142
|
+
await ensure();
|
|
143
|
+
if (next === null && expected === null) {
|
|
144
|
+
// Nothing to write; the claim holds exactly when no live row exists.
|
|
145
|
+
return (await current(key)) === null;
|
|
146
|
+
}
|
|
147
|
+
if (next === null) {
|
|
148
|
+
return ((await driver.run(sql(`DELETE FROM connecta_kv WHERE key = ? AND ${live} AND value = ?`, key, expected))) > 0);
|
|
149
|
+
}
|
|
150
|
+
let expiry;
|
|
151
|
+
try {
|
|
152
|
+
expiry = ttlMillis(opts?.ttlSeconds);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
// Invalid write options do not change a failed comparison's result.
|
|
156
|
+
// Ordinary claims still use one atomic statement without this read.
|
|
157
|
+
if ((await current(key)) !== expected)
|
|
158
|
+
return false;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (expected === null) {
|
|
162
|
+
// Insert, or take over a row that has expired. A live row makes the
|
|
163
|
+
// upsert's WHERE false, so nothing changes and the claim is refused.
|
|
164
|
+
return ((await driver.run(sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
165
|
+
VALUES (?, ?, ${expiryValue})
|
|
166
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
167
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms
|
|
168
|
+
WHERE connecta_kv.expires_at_ms IS NOT NULL
|
|
169
|
+
AND connecta_kv.expires_at_ms <= ${now}`, key, next, expiry))) > 0);
|
|
170
|
+
}
|
|
171
|
+
return ((await driver.run(sql(`UPDATE connecta_kv SET value = ?, expires_at_ms = ${expiryValue}
|
|
172
|
+
WHERE key = ? AND ${live} AND value = ?`, next, expiry, key, expected))) > 0);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Keys one read compares: D1 binds at most 100 parameters, one is `now`. */
|
|
177
|
+
const COPY_READ_KEYS = 99;
|
|
178
|
+
/** Writes one transaction carries, by count and by UTF-8 bytes. */
|
|
179
|
+
const COPY_BATCH_STATEMENTS = 50;
|
|
180
|
+
const COPY_BATCH_BYTES = 4 * 1024 * 1024;
|
|
181
|
+
/**
|
|
182
|
+
* Copy entries into the key-value table, verbatim: same key, same value,
|
|
183
|
+
* same absolute expiry. A live row holding a different value is kept unless
|
|
184
|
+
* `overwrite` is set, so a second copy of the same entries writes nothing.
|
|
185
|
+
* Writes go out in bounded transactions; the outcomes follow `entries`.
|
|
186
|
+
*/
|
|
187
|
+
export async function copyIntoSql(driver, entries, options) {
|
|
188
|
+
const { overwrite, now } = options;
|
|
189
|
+
for (const entry of entries)
|
|
190
|
+
validateStorageKey(entry.key);
|
|
191
|
+
await driver.batch(KV_SCHEMA.map((statement) => sql(statement)));
|
|
192
|
+
const live = "(expires_at_ms IS NULL OR expires_at_ms > ?)";
|
|
193
|
+
const currentValues = async (keys) => {
|
|
194
|
+
const values = new Map();
|
|
195
|
+
for (let start = 0; start < keys.length; start += COPY_READ_KEYS) {
|
|
196
|
+
const chunk = keys.slice(start, start + COPY_READ_KEYS);
|
|
197
|
+
const rows = await driver.all(sql(`SELECT key, ${textColumn("value")}, expires_at_ms FROM connecta_kv
|
|
198
|
+
WHERE key IN (${chunk.map(() => "?").join(", ")}) AND ${live}`, ...chunk, now));
|
|
199
|
+
// The message names no key: a copy's errors reach operator output.
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
values.set(row.key, {
|
|
202
|
+
value: textOf(row.value, row.value_bytes, "a stored value") ?? "",
|
|
203
|
+
expiresAtMs: row.expires_at_ms,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return values;
|
|
208
|
+
};
|
|
209
|
+
const outcomes = Array.from({ length: entries.length });
|
|
210
|
+
const writes = [];
|
|
211
|
+
const existing = await currentValues(entries.map((entry) => entry.key));
|
|
212
|
+
for (const [index, entry] of entries.entries()) {
|
|
213
|
+
const current = existing.get(entry.key);
|
|
214
|
+
if (current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs) {
|
|
215
|
+
outcomes[index] = "unchanged";
|
|
216
|
+
}
|
|
217
|
+
else if (current !== undefined && !overwrite) {
|
|
218
|
+
outcomes[index] = "conflict";
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
outcomes[index] = current === undefined ? "copied" : "overwritten";
|
|
222
|
+
writes.push({
|
|
223
|
+
index,
|
|
224
|
+
bytes: new TextEncoder().encode(entry.key).byteLength + new TextEncoder().encode(entry.value).byteLength + 32,
|
|
225
|
+
statement: overwrite
|
|
226
|
+
? sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
227
|
+
VALUES (?, ?, ?)
|
|
228
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
229
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms`, entry.key, entry.value, entry.expiresAtMs)
|
|
230
|
+
: // Absent when read; a live row written since is kept, not replaced.
|
|
231
|
+
sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
232
|
+
VALUES (?, ?, ?)
|
|
233
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
234
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms
|
|
235
|
+
WHERE connecta_kv.expires_at_ms IS NOT NULL
|
|
236
|
+
AND connecta_kv.expires_at_ms <= ?`, entry.key, entry.value, entry.expiresAtMs, now),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const raced = [];
|
|
241
|
+
let batch = [];
|
|
242
|
+
let batchBytes = 0;
|
|
243
|
+
const flush = async () => {
|
|
244
|
+
if (batch.length === 0)
|
|
245
|
+
return;
|
|
246
|
+
const changes = await driver.batch(batch.map((write) => write.statement));
|
|
247
|
+
for (const [position, write] of batch.entries()) {
|
|
248
|
+
if (!(changes[position] > 0))
|
|
249
|
+
raced.push(write.index);
|
|
250
|
+
}
|
|
251
|
+
batch = [];
|
|
252
|
+
batchBytes = 0;
|
|
253
|
+
};
|
|
254
|
+
for (const write of writes) {
|
|
255
|
+
if (batch.length > 0 && (batch.length >= COPY_BATCH_STATEMENTS || batchBytes + write.bytes > COPY_BATCH_BYTES)) {
|
|
256
|
+
await flush();
|
|
257
|
+
}
|
|
258
|
+
batch.push(write);
|
|
259
|
+
batchBytes += write.bytes;
|
|
260
|
+
}
|
|
261
|
+
await flush();
|
|
262
|
+
// A row another writer added between the read and the write: report what
|
|
263
|
+
// it holds, and leave it.
|
|
264
|
+
if (raced.length > 0) {
|
|
265
|
+
const after = await currentValues(raced.map((index) => entries[index].key));
|
|
266
|
+
for (const index of raced) {
|
|
267
|
+
const entry = entries[index];
|
|
268
|
+
const current = after.get(entry.key);
|
|
269
|
+
outcomes[index] =
|
|
270
|
+
current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs ? "unchanged" : "conflict";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return outcomes;
|
|
274
|
+
}
|
|
275
|
+
// --- activity -----------------------------------------------------------
|
|
276
|
+
/**
|
|
277
|
+
* The activity table, with keyset paging on `(occurred_at_ms, id)`. Its shape
|
|
278
|
+
* is the 0.28 Worker example's `tool_call_activity`; a table created before
|
|
279
|
+
* later actor, friction, approval, client, or package columns existed gets
|
|
280
|
+
* those columns added without changing old rows.
|
|
281
|
+
*/
|
|
282
|
+
const ACTIVITY_SCHEMA = [
|
|
283
|
+
`CREATE TABLE IF NOT EXISTS tool_call_activity (
|
|
284
|
+
id TEXT PRIMARY KEY,
|
|
285
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
286
|
+
request_id TEXT NOT NULL,
|
|
287
|
+
actor_kind TEXT NOT NULL,
|
|
288
|
+
actor_id TEXT,
|
|
289
|
+
actor_namespace TEXT,
|
|
290
|
+
connector_id TEXT NOT NULL,
|
|
291
|
+
tool_name TEXT NOT NULL,
|
|
292
|
+
source TEXT NOT NULL,
|
|
293
|
+
outcome TEXT NOT NULL,
|
|
294
|
+
duration_ms INTEGER NOT NULL,
|
|
295
|
+
attempts INTEGER NOT NULL,
|
|
296
|
+
error_code TEXT,
|
|
297
|
+
friction TEXT,
|
|
298
|
+
approval TEXT,
|
|
299
|
+
package_version TEXT,
|
|
300
|
+
server_name TEXT NOT NULL,
|
|
301
|
+
server_version TEXT NOT NULL,
|
|
302
|
+
client_name TEXT,
|
|
303
|
+
client_version TEXT,
|
|
304
|
+
deployment_id TEXT,
|
|
305
|
+
classification TEXT,
|
|
306
|
+
result_bytes INTEGER,
|
|
307
|
+
event_kind TEXT,
|
|
308
|
+
drift_kind TEXT,
|
|
309
|
+
added_tools INTEGER,
|
|
310
|
+
removed_tools INTEGER,
|
|
311
|
+
changed_tools INTEGER,
|
|
312
|
+
pool_name TEXT,
|
|
313
|
+
actor_basis TEXT
|
|
314
|
+
)`,
|
|
315
|
+
`CREATE INDEX IF NOT EXISTS tool_call_activity_recent
|
|
316
|
+
ON tool_call_activity (occurred_at_ms DESC, id DESC)`,
|
|
317
|
+
];
|
|
318
|
+
/** Columns added after the table first shipped, in the order they arrived. */
|
|
319
|
+
const LATER_ACTIVITY_COLUMNS = [
|
|
320
|
+
"actor_namespace",
|
|
321
|
+
"friction",
|
|
322
|
+
"approval",
|
|
323
|
+
"client_name",
|
|
324
|
+
"client_version",
|
|
325
|
+
"package_version",
|
|
326
|
+
"classification",
|
|
327
|
+
"result_bytes",
|
|
328
|
+
"event_kind",
|
|
329
|
+
"drift_kind",
|
|
330
|
+
"added_tools",
|
|
331
|
+
"removed_tools",
|
|
332
|
+
"changed_tools",
|
|
333
|
+
"pool_name",
|
|
334
|
+
"actor_basis",
|
|
335
|
+
];
|
|
336
|
+
/** Every activity column but the three integers, read through `textOf`. */
|
|
337
|
+
const ACTIVITY_TEXT_COLUMNS = [
|
|
338
|
+
"id",
|
|
339
|
+
"request_id",
|
|
340
|
+
"actor_kind",
|
|
341
|
+
"actor_id",
|
|
342
|
+
"actor_namespace",
|
|
343
|
+
"connector_id",
|
|
344
|
+
"tool_name",
|
|
345
|
+
"source",
|
|
346
|
+
"outcome",
|
|
347
|
+
"error_code",
|
|
348
|
+
"friction",
|
|
349
|
+
"approval",
|
|
350
|
+
"package_version",
|
|
351
|
+
"server_name",
|
|
352
|
+
"server_version",
|
|
353
|
+
"client_name",
|
|
354
|
+
"client_version",
|
|
355
|
+
"deployment_id",
|
|
356
|
+
"classification",
|
|
357
|
+
"event_kind",
|
|
358
|
+
"drift_kind",
|
|
359
|
+
"pool_name",
|
|
360
|
+
"actor_basis",
|
|
361
|
+
];
|
|
362
|
+
const ACTIVITY_SELECT = `SELECT occurred_at_ms, duration_ms, attempts, result_bytes, added_tools, removed_tools, changed_tools,
|
|
363
|
+
${ACTIVITY_TEXT_COLUMNS.map(textColumn).join(",\n ")}
|
|
364
|
+
FROM tool_call_activity`;
|
|
365
|
+
function activityRow(read) {
|
|
366
|
+
const row = {
|
|
367
|
+
occurred_at_ms: read.occurred_at_ms,
|
|
368
|
+
duration_ms: read.duration_ms,
|
|
369
|
+
attempts: read.attempts,
|
|
370
|
+
result_bytes: read.result_bytes,
|
|
371
|
+
added_tools: read.added_tools,
|
|
372
|
+
removed_tools: read.removed_tools,
|
|
373
|
+
changed_tools: read.changed_tools,
|
|
374
|
+
};
|
|
375
|
+
for (const column of ACTIVITY_TEXT_COLUMNS) {
|
|
376
|
+
row[column] = textOf(read[column], read[`${column}_bytes`], `activity column ${column}`);
|
|
377
|
+
}
|
|
378
|
+
return row;
|
|
379
|
+
}
|
|
380
|
+
function rowToEvent(row) {
|
|
381
|
+
// Rows written before `friction` had a column derive it from the code.
|
|
382
|
+
// Friction without a code — an oversized but successful result — exists
|
|
383
|
+
// only in the column, which is why the column exists.
|
|
384
|
+
const friction = row.friction ?? agentFrictionForCode(row.error_code ?? undefined);
|
|
385
|
+
const packageVersion = activityPackageVersion(row.package_version);
|
|
386
|
+
const clientName = activityClientFact(row.client_name, "name");
|
|
387
|
+
const clientVersion = activityClientFact(row.client_version, "version");
|
|
388
|
+
return {
|
|
389
|
+
schemaVersion: 1,
|
|
390
|
+
id: row.id,
|
|
391
|
+
occurredAt: new Date(row.occurred_at_ms).toISOString(),
|
|
392
|
+
requestId: row.request_id,
|
|
393
|
+
...activityBehaviorFacts({
|
|
394
|
+
classification: row.classification,
|
|
395
|
+
resultBytes: row.result_bytes,
|
|
396
|
+
kind: row.event_kind,
|
|
397
|
+
pool: row.pool_name,
|
|
398
|
+
actorBasis: row.actor_basis,
|
|
399
|
+
drift: {
|
|
400
|
+
kind: row.drift_kind,
|
|
401
|
+
addedTools: row.added_tools,
|
|
402
|
+
removedTools: row.removed_tools,
|
|
403
|
+
changedTools: row.changed_tools,
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
actor: {
|
|
407
|
+
kind: row.actor_kind,
|
|
408
|
+
...(row.actor_id ? { id: row.actor_id } : {}),
|
|
409
|
+
...(row.actor_namespace ? { namespace: row.actor_namespace } : {}),
|
|
410
|
+
},
|
|
411
|
+
connectorId: row.connector_id,
|
|
412
|
+
toolName: row.tool_name,
|
|
413
|
+
address: `${row.connector_id}.${row.tool_name}`,
|
|
414
|
+
source: row.source,
|
|
415
|
+
outcome: row.outcome,
|
|
416
|
+
durationMs: row.duration_ms,
|
|
417
|
+
attempts: row.attempts,
|
|
418
|
+
...(row.error_code ? { errorCode: row.error_code } : {}),
|
|
419
|
+
...(friction ? { friction } : {}),
|
|
420
|
+
...(row.approval ? { approval: row.approval } : {}),
|
|
421
|
+
...(packageVersion !== undefined ? { packageVersion } : {}),
|
|
422
|
+
serverName: row.server_name,
|
|
423
|
+
serverVersion: row.server_version,
|
|
424
|
+
...(clientName !== undefined ? { clientName } : {}),
|
|
425
|
+
...(clientVersion !== undefined ? { clientVersion } : {}),
|
|
426
|
+
...(row.deployment_id ? { deploymentId: row.deployment_id } : {}),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function encodeCursor(row) {
|
|
430
|
+
return btoa(`${row.occurred_at_ms}:${row.id}`);
|
|
431
|
+
}
|
|
432
|
+
function decodeCursor(value) {
|
|
433
|
+
let decoded;
|
|
434
|
+
try {
|
|
435
|
+
decoded = atob(value);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
throw new InvalidActivityCursorError();
|
|
439
|
+
}
|
|
440
|
+
const separator = decoded.indexOf(":");
|
|
441
|
+
const occurredAtMs = Number(decoded.slice(0, separator));
|
|
442
|
+
const id = decoded.slice(separator + 1);
|
|
443
|
+
if (separator < 1 || !Number.isSafeInteger(occurredAtMs) || occurredAtMs < 0 || !/^[0-9a-f-]{36}$/i.test(id)) {
|
|
444
|
+
throw new InvalidActivityCursorError();
|
|
445
|
+
}
|
|
446
|
+
return { occurredAtMs, id };
|
|
447
|
+
}
|
|
448
|
+
/** The closed options both activity factories accept; see `assertKnownOptions`. */
|
|
449
|
+
const SQL_ACTIVITY_OPTIONS = optionsOf()(keys("retentionDays"));
|
|
450
|
+
/** Older rows one write removes on its way. Above one: writes catch up. */
|
|
451
|
+
const RETENTION_SWEEP_ROWS = 16;
|
|
452
|
+
/**
|
|
453
|
+
* One append-only row per completed downstream call. Rows carry no arguments,
|
|
454
|
+
* results, generated code, or raw error messages — the store never has a
|
|
455
|
+
* payload to leak.
|
|
456
|
+
*/
|
|
457
|
+
export function sqlActivityStore(driver, kind, options = {}) {
|
|
458
|
+
const factory = kind === "d1" ? "d1ActivityStore()" : "sqliteActivityStore()";
|
|
459
|
+
const { retentionDays = 90 } = assertKnownOptions(options, factory, SQL_ACTIVITY_OPTIONS);
|
|
460
|
+
if (typeof retentionDays !== "number" || !(retentionDays > 0) || !Number.isFinite(retentionDays)) {
|
|
461
|
+
throw new ConfigError(`${factory}.retentionDays must be a positive number of days.`);
|
|
462
|
+
}
|
|
463
|
+
const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
|
|
464
|
+
const ensure = schemaOnce(driver, async (d) => {
|
|
465
|
+
await d.batch(ACTIVITY_SCHEMA.map((statement) => sql(statement)));
|
|
466
|
+
const columns = async () => new Set((await d.all(sql("PRAGMA table_info(tool_call_activity)"))).map((column) => column.name));
|
|
467
|
+
// Isolates and processes upgrade an old table concurrently, each from its
|
|
468
|
+
// own read of the columns. One column per statement, and a refused ALTER
|
|
469
|
+
// re-reads the table: if another upgrader added the column, this one
|
|
470
|
+
// moves on to the next instead of failing the write that started it.
|
|
471
|
+
let present = await columns();
|
|
472
|
+
for (const name of LATER_ACTIVITY_COLUMNS) {
|
|
473
|
+
if (present.has(name))
|
|
474
|
+
continue;
|
|
475
|
+
try {
|
|
476
|
+
await d.run(sql(`ALTER TABLE tool_call_activity ADD COLUMN ${name} ${["result_bytes", "added_tools", "removed_tools", "changed_tools"].includes(name) ? "INTEGER" : "TEXT"}`));
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
present = await columns();
|
|
480
|
+
if (!present.has(name))
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
describe: () => ({ kind, retentionDays }),
|
|
487
|
+
async record(event) {
|
|
488
|
+
await ensure();
|
|
489
|
+
const occurredAtMs = Date.parse(event.occurredAt);
|
|
490
|
+
const facts = activityBehaviorFacts(event);
|
|
491
|
+
await driver.batch([
|
|
492
|
+
sql(`INSERT INTO tool_call_activity (
|
|
493
|
+
id, occurred_at_ms, request_id, actor_kind, actor_id,
|
|
494
|
+
actor_namespace, connector_id, tool_name, source, outcome,
|
|
495
|
+
duration_ms, attempts, error_code, friction, approval,
|
|
496
|
+
package_version, server_name, server_version, client_name, client_version, deployment_id,
|
|
497
|
+
classification, result_bytes, event_kind, drift_kind, added_tools, removed_tools, changed_tools, pool_name, actor_basis
|
|
498
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, event.id, occurredAtMs, event.requestId, event.actor.kind, event.actor.id ?? null, event.actor.namespace ?? null, event.connectorId, event.toolName, event.source, event.outcome, event.durationMs, event.attempts, event.errorCode ?? null,
|
|
499
|
+
// Stored beside the code rather than derived from it: a truncated
|
|
500
|
+
// result is friction on a call that succeeded, and
|
|
501
|
+
// `error_code IS NOT NULL` stays an honest count of failures.
|
|
502
|
+
event.friction ?? null, event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.drift?.kind ?? null, facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
|
|
503
|
+
sql(`DELETE FROM tool_call_activity WHERE id IN (
|
|
504
|
+
SELECT id FROM tool_call_activity
|
|
505
|
+
WHERE occurred_at_ms < ?
|
|
506
|
+
ORDER BY occurred_at_ms ASC
|
|
507
|
+
LIMIT ${RETENTION_SWEEP_ROWS}
|
|
508
|
+
)`, Date.now() - retentionMs),
|
|
509
|
+
]);
|
|
510
|
+
},
|
|
511
|
+
async list({ cursor, limit }) {
|
|
512
|
+
const boundedLimit = Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
513
|
+
const pageSize = boundedLimit + 1;
|
|
514
|
+
const position = cursor ? decodeCursor(cursor) : undefined;
|
|
515
|
+
await ensure();
|
|
516
|
+
const rows = (await driver.all(position
|
|
517
|
+
? sql(`${ACTIVITY_SELECT}
|
|
518
|
+
WHERE occurred_at_ms < ?
|
|
519
|
+
OR (occurred_at_ms = ? AND id < ?)
|
|
520
|
+
ORDER BY occurred_at_ms DESC, id DESC
|
|
521
|
+
LIMIT ?`, position.occurredAtMs, position.occurredAtMs, position.id, pageSize)
|
|
522
|
+
: sql(`${ACTIVITY_SELECT}
|
|
523
|
+
ORDER BY occurred_at_ms DESC, id DESC
|
|
524
|
+
LIMIT ?`, pageSize))).map(activityRow);
|
|
525
|
+
const hasMore = rows.length > boundedLimit;
|
|
526
|
+
const visible = hasMore ? rows.slice(0, boundedLimit) : rows;
|
|
527
|
+
const last = visible.at(-1);
|
|
528
|
+
return {
|
|
529
|
+
events: visible.map(rowToEvent),
|
|
530
|
+
...(hasMore && last ? { nextCursor: encodeCursor(last) } : {}),
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
package/dist/timeout.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// The deadline vocabulary shared by downstream discovery probes and tool calls.
|
|
2
2
|
// One definition keeps those waits bounded consistently.
|
|
3
|
+
import { ConnectorCallError } from "./errors.js";
|
|
4
|
+
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
3
5
|
import { Effect } from "effect";
|
|
4
6
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
5
7
|
/**
|
|
6
8
|
* Generous default bound for a single downstream probe/catalog call. High enough
|
|
7
9
|
* to trip only on a pathological hang, not a realistically slow probe.
|
|
8
10
|
*/
|
|
9
|
-
export const DEFAULT_PROBE_TIMEOUT_MS =
|
|
11
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = CONFIG_DEFAULTS.discovery.probeTimeoutMs;
|
|
10
12
|
/** A finite, positive integer number of milliseconds, or undefined. */
|
|
11
13
|
export function normalizeTimeoutMs(value) {
|
|
12
14
|
if (value === undefined || !Number.isFinite(value) || !(value > 0)) {
|
|
@@ -65,6 +67,6 @@ export async function underAnySignal(signals, operation) {
|
|
|
65
67
|
export function withAbortableTimeout(operation, ms, label) {
|
|
66
68
|
return withDeadline(operation, {
|
|
67
69
|
timeoutMs: ms,
|
|
68
|
-
timeoutError: new
|
|
70
|
+
timeoutError: new ConnectorCallError("timeout", `${label} timed out after ${ms}ms`),
|
|
69
71
|
});
|
|
70
72
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** C0, DEL, and C1 cannot occur in a callable downstream tool name. */
|
|
2
|
+
export function hasControlCharacters(name) {
|
|
3
|
+
for (let index = 0; index < name.length; index++) {
|
|
4
|
+
const code = name.charCodeAt(index);
|
|
5
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
/** Refuse static configuration before errors or warnings can render its names. */
|
|
11
|
+
export function assertStaticToolNames(tools, path) {
|
|
12
|
+
for (const [index, tool] of tools.entries()) {
|
|
13
|
+
if (hasControlCharacters(tool.name)) {
|
|
14
|
+
throw new Error(`${path}[${index}].name contains a control character.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|