@agentic-kit/db-tools 0.2.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.
Files changed (131) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +138 -0
  3. package/app-workspace.d.ts +16 -0
  4. package/app-workspace.js +145 -0
  5. package/context.d.ts +62 -0
  6. package/context.js +282 -0
  7. package/db-probe.d.ts +30 -0
  8. package/db-probe.js +50 -0
  9. package/esm/app-workspace.d.ts +16 -0
  10. package/esm/app-workspace.js +134 -0
  11. package/esm/context.d.ts +62 -0
  12. package/esm/context.js +270 -0
  13. package/esm/db-probe.d.ts +30 -0
  14. package/esm/db-probe.js +47 -0
  15. package/esm/host.d.ts +115 -0
  16. package/esm/host.js +23 -0
  17. package/esm/index.d.ts +20 -0
  18. package/esm/index.js +52 -0
  19. package/esm/policy/add-policies-to-table.d.ts +26 -0
  20. package/esm/policy/add-policies-to-table.js +66 -0
  21. package/esm/policy/provision-helpers.d.ts +38 -0
  22. package/esm/policy/provision-helpers.js +50 -0
  23. package/esm/provision-database/credential.d.ts +14 -0
  24. package/esm/provision-database/credential.js +19 -0
  25. package/esm/provision-database/env-file.d.ts +37 -0
  26. package/esm/provision-database/env-file.js +85 -0
  27. package/esm/provision-database/manifest.d.ts +10 -0
  28. package/esm/provision-database/manifest.js +48 -0
  29. package/esm/provision-database/pg-fixups.d.ts +16 -0
  30. package/esm/provision-database/pg-fixups.js +132 -0
  31. package/esm/provision-database/preset-match.d.ts +13 -0
  32. package/esm/provision-database/preset-match.js +66 -0
  33. package/esm/provision-database/presets.d.ts +4 -0
  34. package/esm/provision-database/presets.js +13 -0
  35. package/esm/provision-database/request-database.d.ts +61 -0
  36. package/esm/provision-database/request-database.js +130 -0
  37. package/esm/provision-database/resolve.d.ts +27 -0
  38. package/esm/provision-database/resolve.js +49 -0
  39. package/esm/records/meta.d.ts +41 -0
  40. package/esm/records/meta.js +167 -0
  41. package/esm/run-codegen/barrels.d.ts +3 -0
  42. package/esm/run-codegen/barrels.js +48 -0
  43. package/esm/run-codegen/endpoints.d.ts +8 -0
  44. package/esm/run-codegen/endpoints.js +46 -0
  45. package/esm/schema-resolve.d.ts +23 -0
  46. package/esm/schema-resolve.js +69 -0
  47. package/esm/tool-schema.d.ts +3 -0
  48. package/esm/tool-schema.js +10 -0
  49. package/esm/tools/add-policies.d.ts +24 -0
  50. package/esm/tools/add-policies.js +94 -0
  51. package/esm/tools/add-records.d.ts +18 -0
  52. package/esm/tools/add-records.js +122 -0
  53. package/esm/tools/add-relation-schema.d.ts +29 -0
  54. package/esm/tools/add-relation-schema.js +56 -0
  55. package/esm/tools/add-relation.d.ts +10 -0
  56. package/esm/tools/add-relation.js +91 -0
  57. package/esm/tools/create-api-key.d.ts +25 -0
  58. package/esm/tools/create-api-key.js +214 -0
  59. package/esm/tools/describe-schema.d.ts +19 -0
  60. package/esm/tools/describe-schema.js +130 -0
  61. package/esm/tools/manage-entity-types.d.ts +40 -0
  62. package/esm/tools/manage-entity-types.js +201 -0
  63. package/esm/tools/mutations.d.ts +35 -0
  64. package/esm/tools/mutations.js +230 -0
  65. package/esm/tools/provision-blueprint.d.ts +21 -0
  66. package/esm/tools/provision-blueprint.js +109 -0
  67. package/esm/tools/provision-database.d.ts +18 -0
  68. package/esm/tools/provision-database.js +260 -0
  69. package/esm/tools/run-codegen.d.ts +12 -0
  70. package/esm/tools/run-codegen.js +137 -0
  71. package/esm/tools/templates.d.ts +71 -0
  72. package/esm/tools/templates.js +331 -0
  73. package/host.d.ts +115 -0
  74. package/host.js +28 -0
  75. package/index.d.ts +20 -0
  76. package/index.js +76 -0
  77. package/package.json +49 -0
  78. package/policy/add-policies-to-table.d.ts +26 -0
  79. package/policy/add-policies-to-table.js +71 -0
  80. package/policy/provision-helpers.d.ts +38 -0
  81. package/policy/provision-helpers.js +58 -0
  82. package/provision-database/credential.d.ts +14 -0
  83. package/provision-database/credential.js +22 -0
  84. package/provision-database/env-file.d.ts +37 -0
  85. package/provision-database/env-file.js +91 -0
  86. package/provision-database/manifest.d.ts +10 -0
  87. package/provision-database/manifest.js +56 -0
  88. package/provision-database/pg-fixups.d.ts +16 -0
  89. package/provision-database/pg-fixups.js +170 -0
  90. package/provision-database/preset-match.d.ts +13 -0
  91. package/provision-database/preset-match.js +70 -0
  92. package/provision-database/presets.d.ts +4 -0
  93. package/provision-database/presets.js +17 -0
  94. package/provision-database/request-database.d.ts +61 -0
  95. package/provision-database/request-database.js +134 -0
  96. package/provision-database/resolve.d.ts +27 -0
  97. package/provision-database/resolve.js +53 -0
  98. package/records/meta.d.ts +41 -0
  99. package/records/meta.js +172 -0
  100. package/run-codegen/barrels.d.ts +3 -0
  101. package/run-codegen/barrels.js +54 -0
  102. package/run-codegen/endpoints.d.ts +8 -0
  103. package/run-codegen/endpoints.js +51 -0
  104. package/schema-resolve.d.ts +23 -0
  105. package/schema-resolve.js +75 -0
  106. package/tool-schema.d.ts +3 -0
  107. package/tool-schema.js +13 -0
  108. package/tools/add-policies.d.ts +24 -0
  109. package/tools/add-policies.js +97 -0
  110. package/tools/add-records.d.ts +18 -0
  111. package/tools/add-records.js +125 -0
  112. package/tools/add-relation-schema.d.ts +29 -0
  113. package/tools/add-relation-schema.js +60 -0
  114. package/tools/add-relation.d.ts +10 -0
  115. package/tools/add-relation.js +94 -0
  116. package/tools/create-api-key.d.ts +25 -0
  117. package/tools/create-api-key.js +220 -0
  118. package/tools/describe-schema.d.ts +19 -0
  119. package/tools/describe-schema.js +133 -0
  120. package/tools/manage-entity-types.d.ts +40 -0
  121. package/tools/manage-entity-types.js +206 -0
  122. package/tools/mutations.d.ts +35 -0
  123. package/tools/mutations.js +233 -0
  124. package/tools/provision-blueprint.d.ts +21 -0
  125. package/tools/provision-blueprint.js +112 -0
  126. package/tools/provision-database.d.ts +18 -0
  127. package/tools/provision-database.js +266 -0
  128. package/tools/run-codegen.d.ts +12 -0
  129. package/tools/run-codegen.js +141 -0
  130. package/tools/templates.d.ts +71 -0
  131. package/tools/templates.js +335 -0
package/esm/context.js ADDED
@@ -0,0 +1,270 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { api, auth, modules } from '@constructive-io/sdk';
4
+ import { parseDotenv } from '12factor-env/dotenv';
5
+ import { probeDatabase } from './db-probe';
6
+ import { DEFAULT_DATA_TOKEN_SKEW_MS, getHost } from './host';
7
+ const DEFAULT_API_ENDPOINT = 'http://api.localhost:3000/graphql';
8
+ const DEFAULT_MODULES_ENDPOINT = 'http://modules.localhost:3000/graphql';
9
+ // The project values pi needs, and where they come from. A scaffolded project
10
+ // folder carries them in `.env` (desktop); a headless host — a container, a
11
+ // Job, CI — injects them as real environment variables, so no credential is
12
+ // ever written into a git clone the agent could commit. Both lanes produce the
13
+ // same record, which is why the resolver takes VALUES, not a directory.
14
+ export const CONTEXT_ENV_KEYS = [
15
+ 'ACCESS_TOKEN',
16
+ 'DATABASE_ID',
17
+ 'API_ENDPOINT',
18
+ 'MODULES_ENDPOINT',
19
+ 'DATABASE_NAME',
20
+ 'OWNER_ID',
21
+ ];
22
+ /** Prefix for injected variables: `ACCESS_TOKEN` is far too generic to claim in
23
+ * a shared process environment, `CONSTRUCTIVE_ACCESS_TOKEN` is not. Inside a
24
+ * project `.env` the bare names stay readable (and are what the scaffolder
25
+ * writes), so both spellings resolve, prefixed winning. */
26
+ export const CONTEXT_ENV_PREFIX = 'CONSTRUCTIVE_';
27
+ const lookup = (source) => typeof source === 'function' ? source : (name) => source[name];
28
+ const readContextValue = (source, key) => {
29
+ const get = lookup(source);
30
+ const value = get(`${CONTEXT_ENV_PREFIX}${key}`) ?? get(key);
31
+ return value ? value : undefined;
32
+ };
33
+ /** Use the process environment (or any injected record) as the source. */
34
+ export function fromEnvironment(environment = process.env) {
35
+ return environment;
36
+ }
37
+ /** Read a project `.env` as the source. The file is authoritative here — the
38
+ * key in it belongs to whatever backend wrote it, so it is not merged under
39
+ * the ambient environment. Returns null when the file is absent. */
40
+ export async function fromEnvFile(cwd) {
41
+ try {
42
+ return parseDotenv(await readFile(path.join(cwd, '.env'), 'utf8'));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Resolve the project context from injected values, or from a project folder.
50
+ *
51
+ * Passing a `cwd` string keeps the original behavior (read `<cwd>/.env`) so
52
+ * existing hosts — Constructive Desktop, the confirm gate — are unchanged.
53
+ * Headless hosts pass values instead: `fromEnvironment()` for a container or
54
+ * Job, an explicit record for anything else.
55
+ */
56
+ export async function resolveProjectContext(input, options = {}) {
57
+ const source = typeof input === 'string' ? await fromEnvFile(input) : input;
58
+ if (!source) {
59
+ return {
60
+ context: null,
61
+ reason: 'No .env found in the project. Provision a Constructive database first (scaffold the project), then retry.',
62
+ code: 'no-env',
63
+ };
64
+ }
65
+ const env = Object.fromEntries(CONTEXT_ENV_KEYS.map((key) => [key, readContextValue(source, key)]));
66
+ const accessToken = env.ACCESS_TOKEN;
67
+ const databaseId = env.DATABASE_ID;
68
+ if (!accessToken || !databaseId) {
69
+ return {
70
+ context: null,
71
+ reason: `Project is not connected to a Constructive database yet (missing ${CONTEXT_ENV_PREFIX}ACCESS_TOKEN/${CONTEXT_ENV_PREFIX}DATABASE_ID in the environment, or ACCESS_TOKEN/DATABASE_ID in .env). Provision the database first, then retry.`,
72
+ code: 'missing-credentials',
73
+ };
74
+ }
75
+ // A source-supplied pin wins for the DATA plane (the access key belongs to
76
+ // whatever backend wrote it); otherwise fall back to the app's backend-config
77
+ // store (environment-aware) so app + harness share one endpoint source.
78
+ const host = getHost();
79
+ const backend = host.backendConfig();
80
+ const apiEndpoint = env.API_ENDPOINT || backend?.apiEndpoint || DEFAULT_API_ENDPOINT;
81
+ const modulesEndpoint = env.MODULES_ENDPOINT || backend?.modulesEndpoint || DEFAULT_MODULES_ENDPOINT;
82
+ // The whole control plane (binding probe, schema resolution, blueprint/schema
83
+ // tools) authenticates with the ACCOUNT bearer: the platform api rejects
84
+ // per-database keys, so the project ACCESS_TOKEN can never act on metaschema
85
+ // surfaces. The source's key stays in the context for the data plane only.
86
+ // The bearer is only ever sent to the app-configured backend — never a
87
+ // source-pinned endpoint, which an untrusted cloned project controls.
88
+ const controlApiEndpoint = backend?.apiEndpoint || DEFAULT_API_ENDPOINT;
89
+ const controlModulesEndpoint = backend?.modulesEndpoint || DEFAULT_MODULES_ENDPOINT;
90
+ const account = host.account();
91
+ const accountBearer = account?.apiKey ?? account?.accessToken;
92
+ if (!accountBearer) {
93
+ return {
94
+ context: null,
95
+ reason: `No usable account credential to reach the Constructive control plane. ${host.signInHint ?? 'Sign in to the app, then retry.'}`,
96
+ code: 'missing-credentials',
97
+ };
98
+ }
99
+ const controlHeaders = { Authorization: `Bearer ${accountBearer}` };
100
+ const apiClient = api.createClient({ endpoint: controlApiEndpoint, headers: controlHeaders });
101
+ // Always probe the bound database — the DATABASE_ID stamp proves it WAS
102
+ // bound, never that the database still exists (backend refresh, deletion,
103
+ // revoked key). The probe is the single source of binding health.
104
+ const probe = await probeDatabase({
105
+ endpoint: controlApiEndpoint,
106
+ bearer: accountBearer,
107
+ databaseId,
108
+ signInHint: host.signInHint,
109
+ });
110
+ if (probe.outcome === 'unreachable') {
111
+ return {
112
+ context: null,
113
+ reason: `Could not reach the Constructive backend at ${controlApiEndpoint} (${probe.detail}). Check it is running, then retry.`,
114
+ code: 'backend-unreachable',
115
+ };
116
+ }
117
+ if (probe.outcome === 'missing') {
118
+ return {
119
+ context: null,
120
+ reason: `The bound database (DATABASE_ID=${databaseId}) no longer exists on this backend. Re-provision with provision_database (reprovision: true) — the schema is rebuilt from the project; records do not carry over.`,
121
+ code: 'db-missing',
122
+ };
123
+ }
124
+ // Schemas are account-scoped: a live binding under a different account is
125
+ // never shown in the Schemas tab or written to by the agent. The project's
126
+ // local blueprint survives account changes, so recovery is a reprovision
127
+ // under the signed-in account. Owner truth comes from the probe (backend),
128
+ // not the source's stamp. Data-plane resolution skips this gate — see
129
+ // ResolveOptions.
130
+ const sessionUserId = account?.userId;
131
+ if (options.plane !== 'data' &&
132
+ probe.ownerId &&
133
+ sessionUserId &&
134
+ probe.ownerId !== sessionUserId) {
135
+ return {
136
+ context: null,
137
+ reason: `The bound database (DATABASE_ID=${databaseId}) was provisioned under a different account than the one signed in. Re-provision with provision_database (reprovision: true) to rebuild it under this account — the schema is rebuilt from the project; records do not carry over.`,
138
+ code: 'account-mismatch',
139
+ };
140
+ }
141
+ const databaseName = env.DATABASE_NAME || probe.name || '';
142
+ const ownerId = probe.ownerId || env.OWNER_ID || undefined;
143
+ const dataEndpoint = databaseName
144
+ ? deriveSubdomainEndpoint(apiEndpoint, `api-${databaseName}`)
145
+ : '';
146
+ const schemaId = await resolveSchemaId(apiClient, databaseId);
147
+ if (!schemaId) {
148
+ return {
149
+ context: null,
150
+ reason: 'Could not resolve a schema (app_public/public) for this database. The database may not be fully provisioned yet.',
151
+ code: 'schema-unresolved',
152
+ };
153
+ }
154
+ return {
155
+ context: {
156
+ api: apiClient,
157
+ modules: modules.createClient({ endpoint: controlModulesEndpoint, headers: controlHeaders }),
158
+ databaseId,
159
+ schemaId,
160
+ ownerId,
161
+ apiEndpoint,
162
+ modulesEndpoint,
163
+ databaseName,
164
+ accessToken,
165
+ dataEndpoint,
166
+ },
167
+ reason: '',
168
+ };
169
+ }
170
+ const needsAuthReason = () => `Not signed in to the app database yet. ${getHost().signInHint ??
171
+ 'Sign in from the Sheets tab, or open the Preview and sign in to your app, then try again.'}`;
172
+ // Resolve a data-plane token for record operations: serve the broker's active
173
+ // account while valid, otherwise harvest the token the user created by signing
174
+ // into their app in the Preview and adopt it into the broker. Also returns the
175
+ // signed-in user's id (used to scope rows by entityId). Returns a reason (no
176
+ // token) when neither source has a valid token — the caller surfaces a sign-in
177
+ // prompt.
178
+ export async function resolveDataToken(context) {
179
+ const host = getHost();
180
+ const broker = host.dataAuthBroker;
181
+ const active = broker?.getActiveToken(context.databaseId);
182
+ if (active)
183
+ return { token: active.token, userId: active.userId };
184
+ const preview = host.previewToken ? await host.previewToken() : null;
185
+ if (!preview)
186
+ return { reason: needsAuthReason() };
187
+ if (broker?.isInvalidToken(context.databaseId, preview.accessToken)) {
188
+ return { reason: needsAuthReason() };
189
+ }
190
+ const parsed = preview.accessTokenExpiresAt ? Date.parse(preview.accessTokenExpiresAt) : NaN;
191
+ const expiresAt = Number.isNaN(parsed) ? Date.now() + 60 * 60 * 1000 : parsed;
192
+ const skewMs = host.dataTokenSkewMs ?? DEFAULT_DATA_TOKEN_SKEW_MS;
193
+ if (expiresAt <= Date.now() + skewMs)
194
+ return { reason: needsAuthReason() };
195
+ broker?.adoptToken(context.databaseId, {
196
+ userId: preview.userId,
197
+ token: preview.accessToken,
198
+ expiresAt,
199
+ origin: 'harvest',
200
+ });
201
+ return { token: preview.accessToken, userId: preview.userId };
202
+ }
203
+ // Per-DB endpoints are deterministic on the provisioning subdomain (= DATABASE_NAME):
204
+ // each plane swaps the first host label of its control-plane endpoint
205
+ // (e.g. api.localhost -> api-myapp.localhost, auth.localhost -> auth-myapp.localhost).
206
+ export function deriveSubdomainEndpoint(baseEndpoint, firstLabel) {
207
+ try {
208
+ const url = new URL(baseEndpoint);
209
+ const labels = url.hostname.split('.');
210
+ labels[0] = firstLabel;
211
+ url.hostname = labels.join('.');
212
+ return url.toString();
213
+ }
214
+ catch {
215
+ return '';
216
+ }
217
+ }
218
+ // The backend hierarchy is org -> db -> tables; the owning org is the auth-plane
219
+ // user the database's ownerId points at. Best-effort display lookup only —
220
+ // binding health never depends on it. Authenticates with the account bearer
221
+ // against the app-configured auth endpoint (the platform rejects per-database
222
+ // keys, and the bearer never goes to a .env-pinned endpoint).
223
+ export async function resolveOrgName(ownerId) {
224
+ try {
225
+ const host = getHost();
226
+ const account = host.account();
227
+ const bearer = account?.apiKey ?? account?.accessToken;
228
+ if (!bearer)
229
+ return undefined;
230
+ const backend = host.backendConfig();
231
+ const authEndpoint = deriveSubdomainEndpoint(backend?.apiEndpoint || DEFAULT_API_ENDPOINT, 'auth');
232
+ if (!authEndpoint)
233
+ return undefined;
234
+ const client = auth.createClient({
235
+ endpoint: authEndpoint,
236
+ headers: { Authorization: `Bearer ${bearer}` },
237
+ });
238
+ const result = await client.user
239
+ .findMany({
240
+ select: { displayName: true, username: true },
241
+ where: { id: { equalTo: ownerId } },
242
+ })
243
+ .execute();
244
+ if (!result.ok)
245
+ return undefined;
246
+ const node = (result.data.users?.nodes ?? [])[0];
247
+ return node?.displayName || node?.username || undefined;
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ }
253
+ async function resolveSchemaId(apiClient, databaseId) {
254
+ const result = await apiClient.schema
255
+ .findMany({
256
+ select: { id: true, name: true },
257
+ where: { databaseId: { equalTo: databaseId } },
258
+ })
259
+ .execute();
260
+ if (!result.ok)
261
+ return undefined;
262
+ const nodes = (result.data.schemas?.nodes ?? []).filter((s) => Boolean(s && s.id));
263
+ const appPublic = nodes.find((s) => s.name === 'app_public');
264
+ if (appPublic)
265
+ return appPublic.id;
266
+ const pub = nodes.find((s) => s.name === 'public');
267
+ if (pub)
268
+ return pub.id;
269
+ return nodes[0]?.id;
270
+ }
@@ -0,0 +1,30 @@
1
+ export type DatabaseProbe = {
2
+ outcome: 'found';
3
+ name?: string;
4
+ ownerId?: string;
5
+ } | {
6
+ outcome: 'missing';
7
+ } | {
8
+ outcome: 'unreachable';
9
+ detail: string;
10
+ };
11
+ export type ProbeExecutor = {
12
+ execute<T>(document: string, variables?: Record<string, unknown>): Promise<{
13
+ ok: true;
14
+ data: T;
15
+ errors: undefined;
16
+ } | {
17
+ ok: false;
18
+ data: null;
19
+ errors: {
20
+ message: string;
21
+ }[];
22
+ }>;
23
+ };
24
+ export declare function probeDatabase(args: {
25
+ endpoint: string;
26
+ bearer: string;
27
+ databaseId: string;
28
+ executor?: ProbeExecutor;
29
+ signInHint?: string;
30
+ }): Promise<DatabaseProbe>;
@@ -0,0 +1,47 @@
1
+ import { api } from '@constructive-io/sdk';
2
+ const AUTH_ERROR_RE = /unauthenticated|unauthorized|permission denied|jwt|http 401|http 403/i;
3
+ // Liveness probe for a bound database. The split drives recovery: 'missing'
4
+ // means the backend answered and the binding is dead (database deleted,
5
+ // backend refreshed) — recovery is reprovision. 'unreachable' means no usable
6
+ // answer (backend down, network error, rejected credential) — recovery is
7
+ // retry/re-sign-in, never reprovision.
8
+ //
9
+ // The probe authenticates with the ACCOUNT bearer, not the project's .env key:
10
+ // the platform api rejects per-database keys, and the singular `database(id:)`
11
+ // field is gone from the current contract, so the lookup goes through the
12
+ // `databases` collection filter via the SDK's raw FetchAdapter.
13
+ export async function probeDatabase(args) {
14
+ const executor = args.executor ??
15
+ new api.FetchAdapter(args.endpoint, { Authorization: `Bearer ${args.bearer}` });
16
+ const document = `
17
+ query ProbeDatabase {
18
+ databases(where: { id: { equalTo: ${JSON.stringify(args.databaseId)} } }) {
19
+ nodes { id name ownerId }
20
+ }
21
+ }`;
22
+ try {
23
+ const result = await executor.execute(document);
24
+ if (result.ok) {
25
+ const node = result.data.databases?.nodes?.[0];
26
+ if (!node)
27
+ return { outcome: 'missing' };
28
+ return { outcome: 'found', name: node.name ?? undefined, ownerId: node.ownerId ?? undefined };
29
+ }
30
+ const detail = result.errors?.[0]?.message ?? 'unknown error';
31
+ // A 5xx is the backend failing, not an answer about this database. An auth
32
+ // rejection is a dead ACCOUNT credential — never evidence the binding is
33
+ // dead, so it must not push toward reprovision.
34
+ if (/^HTTP 5\d\d/i.test(detail))
35
+ return { outcome: 'unreachable', detail };
36
+ if (AUTH_ERROR_RE.test(detail)) {
37
+ return {
38
+ outcome: 'unreachable',
39
+ detail: `${detail} — the account credential was rejected. ${args.signInHint ?? 'Sign in again, then retry.'}`,
40
+ };
41
+ }
42
+ return { outcome: 'missing' };
43
+ }
44
+ catch (err) {
45
+ return { outcome: 'unreachable', detail: err instanceof Error ? err.message : String(err) };
46
+ }
47
+ }
package/esm/host.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Host contract for the Constructive database tools.
3
+ *
4
+ * The typed db tools were extracted from Constructive Desktop, where they read
5
+ * an Electron-side `runtime` singleton (account store, backend config, data-auth
6
+ * broker, preview token). Hosts now inject the same surface here once at
7
+ * startup (`configureHost`); the tool modules stay module-level `HarnessTool`
8
+ * consts and read it lazily via `getHost()`.
9
+ *
10
+ * Nothing here is harness-specific: the host is the *application* a tool acts
11
+ * on behalf of, so the same contract serves whichever adapter runs the tools.
12
+ */
13
+ export type HostAccount = {
14
+ userId: string;
15
+ accessToken: string;
16
+ apiKey?: string;
17
+ };
18
+ export type HostBackendConfig = {
19
+ apiEndpoint?: string;
20
+ modulesEndpoint?: string;
21
+ };
22
+ export type ActiveDataToken = {
23
+ token: string;
24
+ userId?: string;
25
+ expiresAt: number;
26
+ origin?: string;
27
+ };
28
+ /**
29
+ * Optional data-plane token broker: remembers per-database end-user tokens
30
+ * across tool calls and invalidates declined/expired ones. Hosts without a
31
+ * broker fall back to the preview-token harvest on every call.
32
+ */
33
+ export interface DataAuthBroker {
34
+ getActiveToken(databaseId: string): ActiveDataToken | undefined | null;
35
+ isInvalidToken(databaseId: string, token: string): boolean;
36
+ adoptToken(databaseId: string, token: ActiveDataToken): void;
37
+ }
38
+ /** Token harvested from the host's app preview (end-user sign-in). */
39
+ export type PreviewToken = {
40
+ accessToken: string;
41
+ accessTokenExpiresAt?: string;
42
+ userId?: string;
43
+ };
44
+ /**
45
+ * Overlay layered over the pinned base preset when provisioning a database.
46
+ * Structurally the `ProvisionOverlay` from `provision-database/resolve`; typed
47
+ * loosely here to keep `host.ts` free of provision-internal imports.
48
+ */
49
+ export interface HostProvisionOverlay {
50
+ preset?: string;
51
+ add?: (string | [string, Record<string, unknown>])[];
52
+ remove?: string[];
53
+ }
54
+ /**
55
+ * A minted secret handed to the host for out-of-band delivery (.env write +
56
+ * one-time reveal). The plaintext never enters tool results or the transcript;
57
+ * the harness forgets it after this call.
58
+ */
59
+ export type SecretDelivery = {
60
+ databaseId: string;
61
+ /** Project directory whose `.env` receives the key. */
62
+ cwd: string;
63
+ envVar: string;
64
+ plaintext: string;
65
+ keyId: string;
66
+ expiresAt?: string;
67
+ };
68
+ /**
69
+ * Context for a host-side step-up: enough to derive the per-database auth
70
+ * endpoint and look up the app session without re-resolving the project.
71
+ */
72
+ export type StepUpRequest = {
73
+ databaseId: string;
74
+ databaseName: string;
75
+ apiEndpoint: string;
76
+ };
77
+ export interface ToolsHost {
78
+ /** Signed-in platform account, or null/undefined when signed out. */
79
+ account(): HostAccount | null | undefined;
80
+ /** Host-configured backend endpoints (env-aware). */
81
+ backendConfig(): HostBackendConfig | null | undefined;
82
+ /** Optional data-plane token broker (see DataAuthBroker). */
83
+ dataAuthBroker?: DataAuthBroker;
84
+ /**
85
+ * Host-specific sign-in instruction, substituted into signed-out failure
86
+ * reasons (e.g. the CLI's "Run `agent login` to sign in."). Absent hosts get
87
+ * the desktop wording.
88
+ */
89
+ signInHint?: string;
90
+ /** Harvest an end-user token from the host's app preview, if it has one. */
91
+ previewToken?(): Promise<PreviewToken | null>;
92
+ /** Treat tokens expiring within this window as already expired. Default 30s. */
93
+ dataTokenSkewMs?: number;
94
+ /**
95
+ * Optional provision overlay: pick a base preset and/or layer module
96
+ * add/remove on top of it. The base module list always comes from the pinned
97
+ * `node-type-registry` preset — this only customizes it. Distributed as data
98
+ * (e.g. materialized from appstash / a pinned git ref), never as code.
99
+ */
100
+ provisionOverlay?(): HostProvisionOverlay | null | undefined | Promise<HostProvisionOverlay | null | undefined>;
101
+ /**
102
+ * Complete MFA step-up for the database's app session in the host's own
103
+ * process (password dialog + verifyPassword). The password never passes
104
+ * through the harness or the model. Resolve true when step-up succeeded.
105
+ */
106
+ requestStepUp?(request: StepUpRequest): Promise<boolean>;
107
+ /**
108
+ * Deliver a minted secret to the user (.env write + one-time reveal).
109
+ * Required for create_api_key — without it the tool refuses to mint.
110
+ */
111
+ deliverSecret?(delivery: SecretDelivery): Promise<void>;
112
+ }
113
+ export declare const DEFAULT_DATA_TOKEN_SKEW_MS = 30000;
114
+ export declare function configureHost(host: ToolsHost): void;
115
+ export declare function getHost(): ToolsHost;
package/esm/host.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Host contract for the Constructive database tools.
3
+ *
4
+ * The typed db tools were extracted from Constructive Desktop, where they read
5
+ * an Electron-side `runtime` singleton (account store, backend config, data-auth
6
+ * broker, preview token). Hosts now inject the same surface here once at
7
+ * startup (`configureHost`); the tool modules stay module-level `HarnessTool`
8
+ * consts and read it lazily via `getHost()`.
9
+ *
10
+ * Nothing here is harness-specific: the host is the *application* a tool acts
11
+ * on behalf of, so the same contract serves whichever adapter runs the tools.
12
+ */
13
+ export const DEFAULT_DATA_TOKEN_SKEW_MS = 30_000;
14
+ let currentHost = null;
15
+ export function configureHost(host) {
16
+ currentHost = host;
17
+ }
18
+ export function getHost() {
19
+ if (!currentHost) {
20
+ throw new Error('@agentic-kit/db-tools host not configured. Call configureHost() (or createDbTools(host)) before using the db tools.');
21
+ }
22
+ return currentHost;
23
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { AnyHarnessTool } from '@agentic-kit/harness';
2
+ import { type ToolsHost } from './host';
3
+ /**
4
+ * The Constructive database tools, in registration order.
5
+ *
6
+ * Plain `HarnessTool`s: a harness gets them by mapping them into its own tool
7
+ * shape, which is the adapter's job, so this package stays free of any harness
8
+ * dependency (see `toPiTool` in `@agentic-kit/pi` for that binding).
9
+ */
10
+ export declare const constructiveDbTools: readonly AnyHarnessTool[];
11
+ /** Configure the host and get the tools in one call. */
12
+ export declare function createConstructiveDbTools(host: ToolsHost): readonly AnyHarnessTool[];
13
+ export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, type ContextEnvKey, type ContextSource, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, type ModulesClient, type ProjectContext, type ProjectContextFailureCode, resolveDataToken, resolveProjectContext, } from './context';
14
+ export { type ActiveDataToken, configureHost, type DataAuthBroker, DEFAULT_DATA_TOKEN_SKEW_MS, getHost, type HostAccount, type HostBackendConfig, type HostProvisionOverlay, type PreviewToken, type SecretDelivery, type StepUpRequest, type ToolsHost, } from './host';
15
+ export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, type ProvisionManifest, } from './provision-database/manifest';
16
+ export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, type ModulePreset, type ProvisionModule, } from './provision-database/presets';
17
+ export { moduleKey, type ProvisionOverlay, resolveProvisionModules, } from './provision-database/resolve';
18
+ export { toolSchema } from './tool-schema';
19
+ export { createTemplatePreviewTables } from './tools/templates';
20
+ export default constructiveDbTools;
package/esm/index.js ADDED
@@ -0,0 +1,52 @@
1
+ import { configureHost } from './host';
2
+ import { addPoliciesTool } from './tools/add-policies';
3
+ import { addRecordsTool } from './tools/add-records';
4
+ import { addRelationTool } from './tools/add-relation';
5
+ import { createApiKeyTool } from './tools/create-api-key';
6
+ import { describeSchemaTool } from './tools/describe-schema';
7
+ import { manageEntityTypesTool } from './tools/manage-entity-types';
8
+ import { createFieldTool, deleteFieldTool, deleteTableTool, updateFieldTool } from './tools/mutations';
9
+ import { provisionBlueprintTool } from './tools/provision-blueprint';
10
+ import { provisionDatabaseTool } from './tools/provision-database';
11
+ import { runCodegenTool } from './tools/run-codegen';
12
+ import { applyTemplateTool, createTemplateTool, deleteTemplateTool, listTemplatesTool, updateTemplateTool, } from './tools/templates';
13
+ /**
14
+ * The Constructive database tools, in registration order.
15
+ *
16
+ * Plain `HarnessTool`s: a harness gets them by mapping them into its own tool
17
+ * shape, which is the adapter's job, so this package stays free of any harness
18
+ * dependency (see `toPiTool` in `@agentic-kit/pi` for that binding).
19
+ */
20
+ export const constructiveDbTools = [
21
+ provisionDatabaseTool,
22
+ describeSchemaTool,
23
+ listTemplatesTool,
24
+ provisionBlueprintTool,
25
+ addRelationTool,
26
+ deleteTableTool,
27
+ createFieldTool,
28
+ updateFieldTool,
29
+ deleteFieldTool,
30
+ addPoliciesTool,
31
+ applyTemplateTool,
32
+ createTemplateTool,
33
+ updateTemplateTool,
34
+ deleteTemplateTool,
35
+ addRecordsTool,
36
+ manageEntityTypesTool,
37
+ createApiKeyTool,
38
+ runCodegenTool,
39
+ ];
40
+ /** Configure the host and get the tools in one call. */
41
+ export function createConstructiveDbTools(host) {
42
+ configureHost(host);
43
+ return constructiveDbTools;
44
+ }
45
+ export { CONTEXT_ENV_KEYS, CONTEXT_ENV_PREFIX, deriveSubdomainEndpoint, fromEnvFile, fromEnvironment, resolveDataToken, resolveProjectContext, } from './context';
46
+ export { configureHost, DEFAULT_DATA_TOKEN_SKEW_MS, getHost, } from './host';
47
+ export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, } from './provision-database/manifest';
48
+ export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, } from './provision-database/presets';
49
+ export { moduleKey, resolveProvisionModules, } from './provision-database/resolve';
50
+ export { toolSchema } from './tool-schema';
51
+ export { createTemplatePreviewTables } from './tools/templates';
52
+ export default constructiveDbTools;
@@ -0,0 +1,26 @@
1
+ import { type PolicyProvisioningCategory } from '@agentic-kit/harness';
2
+ import type { ModulesClient } from '../context';
3
+ import { type CrudOperation, type CrudPolicyConfigs } from './provision-helpers';
4
+ export interface AddPoliciesToTablePolicyEntry {
5
+ policyType: string;
6
+ dataNodeType?: string;
7
+ nodeData?: Record<string, unknown>;
8
+ sharedPolicyData: Record<string, unknown>;
9
+ operations: CrudPolicyConfigs;
10
+ enabledOperations?: CrudOperation[];
11
+ }
12
+ export interface AddPoliciesToTableInput {
13
+ client: ModulesClient;
14
+ databaseId: string;
15
+ schemaId: string;
16
+ tableId: string;
17
+ policies: AddPoliciesToTablePolicyEntry[];
18
+ }
19
+ export declare class UnsupportedPolicyCategoryError extends Error {
20
+ readonly policyType: string;
21
+ readonly category: PolicyProvisioningCategory;
22
+ constructor(policyType: string, category: PolicyProvisioningCategory);
23
+ }
24
+ export declare function addPoliciesToExistingTable(input: AddPoliciesToTableInput): Promise<{
25
+ success: true;
26
+ }>;
@@ -0,0 +1,66 @@
1
+ import { getPolicyCategory } from '@agentic-kit/harness';
2
+ import { buildGrants, buildPolicyEntry, CRUD_OPERATIONS, groupOperationsByConfig, } from './provision-helpers';
3
+ const SUPPORTED_CATEGORIES = new Set([
4
+ 'has-module',
5
+ 'no-fields',
6
+ 'needs-table',
7
+ ]);
8
+ export class UnsupportedPolicyCategoryError extends Error {
9
+ policyType;
10
+ category;
11
+ constructor(policyType, category) {
12
+ super(`Policy type "${policyType}" has category "${category}" which requires column creation or composite handling — use the policies UI instead.`);
13
+ this.policyType = policyType;
14
+ this.category = category;
15
+ this.name = 'UnsupportedPolicyCategoryError';
16
+ }
17
+ }
18
+ export async function addPoliciesToExistingTable(input) {
19
+ const { client } = input;
20
+ for (const entry of input.policies) {
21
+ const category = getPolicyCategory(entry.policyType);
22
+ if (!SUPPORTED_CATEGORIES.has(category)) {
23
+ throw new UnsupportedPolicyCategoryError(entry.policyType, category);
24
+ }
25
+ }
26
+ const policiesArray = [];
27
+ const nodesByType = new Map();
28
+ for (const entry of input.policies) {
29
+ const groups = groupOperationsByConfig(entry.enabledOperations ?? CRUD_OPERATIONS, entry.operations);
30
+ for (const group of groups) {
31
+ const ops = group.privileges.join('_');
32
+ const rand = Math.random().toString(36).slice(2, 8);
33
+ policiesArray.push(buildPolicyEntry(entry.policyType, {
34
+ privileges: group.privileges,
35
+ policy_role: group.roleName,
36
+ permissive: group.isPermissive,
37
+ data: entry.sharedPolicyData,
38
+ policy_name: `${ops}_${rand}`,
39
+ }));
40
+ }
41
+ if (entry.dataNodeType && !nodesByType.has(entry.dataNodeType)) {
42
+ const nodeData = entry.nodeData ?? {};
43
+ nodesByType.set(entry.dataNodeType, {
44
+ $type: entry.dataNodeType,
45
+ ...(Object.keys(nodeData).length > 0 ? { data: nodeData } : {}),
46
+ });
47
+ }
48
+ }
49
+ const provisionInput = {
50
+ databaseId: input.databaseId,
51
+ schemaId: input.schemaId,
52
+ tableId: input.tableId,
53
+ grants: buildGrants(),
54
+ policies: policiesArray,
55
+ };
56
+ if (nodesByType.size > 0) {
57
+ provisionInput.nodes = [...nodesByType.values()];
58
+ }
59
+ await client.secureTableProvision
60
+ .create({
61
+ data: provisionInput,
62
+ select: { id: true, tableId: true, tableName: true },
63
+ })
64
+ .unwrap();
65
+ return { success: true };
66
+ }