@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.
- package/LICENSE +23 -0
- package/README.md +138 -0
- package/app-workspace.d.ts +16 -0
- package/app-workspace.js +145 -0
- package/context.d.ts +62 -0
- package/context.js +282 -0
- package/db-probe.d.ts +30 -0
- package/db-probe.js +50 -0
- package/esm/app-workspace.d.ts +16 -0
- package/esm/app-workspace.js +134 -0
- package/esm/context.d.ts +62 -0
- package/esm/context.js +270 -0
- package/esm/db-probe.d.ts +30 -0
- package/esm/db-probe.js +47 -0
- package/esm/host.d.ts +115 -0
- package/esm/host.js +23 -0
- package/esm/index.d.ts +20 -0
- package/esm/index.js +52 -0
- package/esm/policy/add-policies-to-table.d.ts +26 -0
- package/esm/policy/add-policies-to-table.js +66 -0
- package/esm/policy/provision-helpers.d.ts +38 -0
- package/esm/policy/provision-helpers.js +50 -0
- package/esm/provision-database/credential.d.ts +14 -0
- package/esm/provision-database/credential.js +19 -0
- package/esm/provision-database/env-file.d.ts +37 -0
- package/esm/provision-database/env-file.js +85 -0
- package/esm/provision-database/manifest.d.ts +10 -0
- package/esm/provision-database/manifest.js +48 -0
- package/esm/provision-database/pg-fixups.d.ts +16 -0
- package/esm/provision-database/pg-fixups.js +132 -0
- package/esm/provision-database/preset-match.d.ts +13 -0
- package/esm/provision-database/preset-match.js +66 -0
- package/esm/provision-database/presets.d.ts +4 -0
- package/esm/provision-database/presets.js +13 -0
- package/esm/provision-database/request-database.d.ts +61 -0
- package/esm/provision-database/request-database.js +130 -0
- package/esm/provision-database/resolve.d.ts +27 -0
- package/esm/provision-database/resolve.js +49 -0
- package/esm/records/meta.d.ts +41 -0
- package/esm/records/meta.js +167 -0
- package/esm/run-codegen/barrels.d.ts +3 -0
- package/esm/run-codegen/barrels.js +48 -0
- package/esm/run-codegen/endpoints.d.ts +8 -0
- package/esm/run-codegen/endpoints.js +46 -0
- package/esm/schema-resolve.d.ts +23 -0
- package/esm/schema-resolve.js +69 -0
- package/esm/tool-schema.d.ts +3 -0
- package/esm/tool-schema.js +10 -0
- package/esm/tools/add-policies.d.ts +24 -0
- package/esm/tools/add-policies.js +94 -0
- package/esm/tools/add-records.d.ts +18 -0
- package/esm/tools/add-records.js +122 -0
- package/esm/tools/add-relation-schema.d.ts +29 -0
- package/esm/tools/add-relation-schema.js +56 -0
- package/esm/tools/add-relation.d.ts +10 -0
- package/esm/tools/add-relation.js +91 -0
- package/esm/tools/create-api-key.d.ts +25 -0
- package/esm/tools/create-api-key.js +214 -0
- package/esm/tools/describe-schema.d.ts +19 -0
- package/esm/tools/describe-schema.js +130 -0
- package/esm/tools/manage-entity-types.d.ts +40 -0
- package/esm/tools/manage-entity-types.js +201 -0
- package/esm/tools/mutations.d.ts +35 -0
- package/esm/tools/mutations.js +230 -0
- package/esm/tools/provision-blueprint.d.ts +21 -0
- package/esm/tools/provision-blueprint.js +109 -0
- package/esm/tools/provision-database.d.ts +18 -0
- package/esm/tools/provision-database.js +260 -0
- package/esm/tools/run-codegen.d.ts +12 -0
- package/esm/tools/run-codegen.js +137 -0
- package/esm/tools/templates.d.ts +71 -0
- package/esm/tools/templates.js +331 -0
- package/host.d.ts +115 -0
- package/host.js +28 -0
- package/index.d.ts +20 -0
- package/index.js +76 -0
- package/package.json +49 -0
- package/policy/add-policies-to-table.d.ts +26 -0
- package/policy/add-policies-to-table.js +71 -0
- package/policy/provision-helpers.d.ts +38 -0
- package/policy/provision-helpers.js +58 -0
- package/provision-database/credential.d.ts +14 -0
- package/provision-database/credential.js +22 -0
- package/provision-database/env-file.d.ts +37 -0
- package/provision-database/env-file.js +91 -0
- package/provision-database/manifest.d.ts +10 -0
- package/provision-database/manifest.js +56 -0
- package/provision-database/pg-fixups.d.ts +16 -0
- package/provision-database/pg-fixups.js +170 -0
- package/provision-database/preset-match.d.ts +13 -0
- package/provision-database/preset-match.js +70 -0
- package/provision-database/presets.d.ts +4 -0
- package/provision-database/presets.js +17 -0
- package/provision-database/request-database.d.ts +61 -0
- package/provision-database/request-database.js +134 -0
- package/provision-database/resolve.d.ts +27 -0
- package/provision-database/resolve.js +53 -0
- package/records/meta.d.ts +41 -0
- package/records/meta.js +172 -0
- package/run-codegen/barrels.d.ts +3 -0
- package/run-codegen/barrels.js +54 -0
- package/run-codegen/endpoints.d.ts +8 -0
- package/run-codegen/endpoints.js +51 -0
- package/schema-resolve.d.ts +23 -0
- package/schema-resolve.js +75 -0
- package/tool-schema.d.ts +3 -0
- package/tool-schema.js +13 -0
- package/tools/add-policies.d.ts +24 -0
- package/tools/add-policies.js +97 -0
- package/tools/add-records.d.ts +18 -0
- package/tools/add-records.js +125 -0
- package/tools/add-relation-schema.d.ts +29 -0
- package/tools/add-relation-schema.js +60 -0
- package/tools/add-relation.d.ts +10 -0
- package/tools/add-relation.js +94 -0
- package/tools/create-api-key.d.ts +25 -0
- package/tools/create-api-key.js +220 -0
- package/tools/describe-schema.d.ts +19 -0
- package/tools/describe-schema.js +133 -0
- package/tools/manage-entity-types.d.ts +40 -0
- package/tools/manage-entity-types.js +206 -0
- package/tools/mutations.d.ts +35 -0
- package/tools/mutations.js +233 -0
- package/tools/provision-blueprint.d.ts +21 -0
- package/tools/provision-blueprint.js +112 -0
- package/tools/provision-database.d.ts +18 -0
- package/tools/provision-database.js +266 -0
- package/tools/run-codegen.d.ts +12 -0
- package/tools/run-codegen.js +141 -0
- package/tools/templates.d.ts +71 -0
- package/tools/templates.js +335 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiKeyTool = void 0;
|
|
4
|
+
exports.toEnvVar = toEnvVar;
|
|
5
|
+
exports.isStepUpError = isStepUpError;
|
|
6
|
+
exports.describeScope = describeScope;
|
|
7
|
+
const sdk_1 = require("@constructive-io/sdk");
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const context_1 = require("../context");
|
|
10
|
+
const host_1 = require("../host");
|
|
11
|
+
const CreateApiKeyZod = zod_1.z.object({
|
|
12
|
+
key_name: zod_1.z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('Name for the API key (e.g. "deploy bot"). The .env variable name derives from it (UPPER_SNAKE + _API_KEY).'),
|
|
15
|
+
principal_name: zod_1.z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('Machine identity (principal) that owns the key. Defaults to key_name. An existing principal with this name is reused for unscoped keys.')
|
|
18
|
+
.optional(),
|
|
19
|
+
entity_ids: zod_1.z
|
|
20
|
+
.array(zod_1.z.string())
|
|
21
|
+
.describe('Entity row UUIDs to scope the principal to. Omit for a personal (unscoped) key that acts as the signed-in app user.')
|
|
22
|
+
.optional(),
|
|
23
|
+
read_only: zod_1.z
|
|
24
|
+
.boolean()
|
|
25
|
+
.describe('Restrict the principal and key to read-only operations.')
|
|
26
|
+
.optional(),
|
|
27
|
+
expires_in_days: zod_1.z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.positive()
|
|
31
|
+
.describe('Key lifetime in days. Default 90.')
|
|
32
|
+
.optional(),
|
|
33
|
+
});
|
|
34
|
+
function fail(message, extra) {
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: message }],
|
|
37
|
+
details: { success: false, message, ...extra },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toEnvVar(keyName) {
|
|
41
|
+
const base = keyName
|
|
42
|
+
.trim()
|
|
43
|
+
.toUpperCase()
|
|
44
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
45
|
+
.replace(/^_+|_+$/g, '');
|
|
46
|
+
return base.endsWith('_API_KEY') ? base : `${base}_API_KEY`;
|
|
47
|
+
}
|
|
48
|
+
function isStepUpError(messages) {
|
|
49
|
+
return messages.some((m) => /step[\s_-]?up/i.test(m));
|
|
50
|
+
}
|
|
51
|
+
function describeScope(entityIds, readOnly) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
parts.push(entityIds?.length
|
|
54
|
+
? `scoped to ${entityIds.length} entit${entityIds.length === 1 ? 'y' : 'ies'}`
|
|
55
|
+
: 'unscoped (acts as the signed-in app user)');
|
|
56
|
+
if (readOnly)
|
|
57
|
+
parts.push('read-only');
|
|
58
|
+
return parts.join(', ');
|
|
59
|
+
}
|
|
60
|
+
async function rawGraphql(endpoint, token, query, variables) {
|
|
61
|
+
const res = await fetch(endpoint, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({ query, variables }),
|
|
68
|
+
});
|
|
69
|
+
return (await res.json());
|
|
70
|
+
}
|
|
71
|
+
async function createPrincipalInputFields(endpoint) {
|
|
72
|
+
const probe = await rawGraphql(endpoint, undefined, '{ __type(name: "CreatePrincipalInput") { inputFields { name } } }');
|
|
73
|
+
const type = probe.data?.__type;
|
|
74
|
+
return new Set((type?.inputFields ?? []).map((f) => f.name));
|
|
75
|
+
}
|
|
76
|
+
exports.createApiKeyTool = {
|
|
77
|
+
name: 'create_api_key',
|
|
78
|
+
label: 'Create API key',
|
|
79
|
+
description: 'Mint an API key for the project database, owned by a machine principal. Optionally scope it to specific entity rows and/or read-only. The human confirms the mint, completes MFA step-up in the app when required, and receives the key via .env + a one-time reveal — the plaintext key never appears in this conversation. Requires the user to be signed in to their app in the Preview.',
|
|
80
|
+
promptSnippet: 'create_api_key: mint an entity-scoped API key for the project database. Gated; the plaintext goes to .env + a one-time reveal, never into the chat — reference it as its env var.',
|
|
81
|
+
parameters: CreateApiKeyZod,
|
|
82
|
+
async execute(params, ctx) {
|
|
83
|
+
const keyName = params.key_name?.trim();
|
|
84
|
+
if (!keyName)
|
|
85
|
+
return fail('create_api_key requires "key_name".');
|
|
86
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd, { plane: 'data' });
|
|
87
|
+
if (!resolved.context)
|
|
88
|
+
return fail(resolved.reason);
|
|
89
|
+
const { databaseId, databaseName, apiEndpoint } = resolved.context;
|
|
90
|
+
if (!databaseName) {
|
|
91
|
+
return fail('Cannot resolve the app auth endpoint (DATABASE_NAME missing from .env). Re-provision the database, then retry.');
|
|
92
|
+
}
|
|
93
|
+
const host = (0, host_1.getHost)();
|
|
94
|
+
if (!host.deliverSecret) {
|
|
95
|
+
return fail('This host cannot deliver secrets (no .env write + reveal flow), so no key was minted. Create the key from a host that supports secret delivery.');
|
|
96
|
+
}
|
|
97
|
+
const token = await (0, context_1.resolveDataToken)(resolved.context);
|
|
98
|
+
if (!token.token) {
|
|
99
|
+
return fail(token.reason ?? 'Sign in to the app database to create an API key.', {
|
|
100
|
+
needsAuth: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const authEndpoint = (0, context_1.deriveSubdomainEndpoint)(apiEndpoint, `auth-${databaseName}`);
|
|
104
|
+
if (!authEndpoint)
|
|
105
|
+
return fail('Cannot derive the app auth endpoint.');
|
|
106
|
+
const scoped = Boolean(params.entity_ids?.length) || params.read_only === true;
|
|
107
|
+
try {
|
|
108
|
+
if (scoped) {
|
|
109
|
+
const fields = await createPrincipalInputFields(authEndpoint);
|
|
110
|
+
const missing = [
|
|
111
|
+
...(params.entity_ids?.length && !fields.has('entityIds') ? ['entityIds'] : []),
|
|
112
|
+
...(params.read_only === true && !fields.has('isReadOnly') ? ['isReadOnly'] : []),
|
|
113
|
+
];
|
|
114
|
+
if (missing.length) {
|
|
115
|
+
return fail(`This deployment does not support scoping a principal at create time (no ${missing.join(', ')} on createPrincipal). No key was minted. If an unscoped key that acts as the signed-in user is acceptable, ask for one explicitly.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const dbAuth = sdk_1.auth.createClient({
|
|
119
|
+
endpoint: authEndpoint,
|
|
120
|
+
headers: { Authorization: `Bearer ${token.token}` },
|
|
121
|
+
});
|
|
122
|
+
const principalName = params.principal_name?.trim() || keyName;
|
|
123
|
+
const existing = await dbAuth.principal
|
|
124
|
+
.findFirst({
|
|
125
|
+
where: { name: { equalTo: principalName } },
|
|
126
|
+
select: { id: true, name: true, userId: true, isReadOnly: true },
|
|
127
|
+
})
|
|
128
|
+
.unwrap();
|
|
129
|
+
// createApiKey (like createPrincipal.result) takes the principal's
|
|
130
|
+
// identity userId; principal_entity.principalId references the row id.
|
|
131
|
+
let principalId = existing.principal?.userId;
|
|
132
|
+
if (existing.principal && scoped) {
|
|
133
|
+
return fail(`Principal "${principalName}" already exists, and its scope cannot be verified against the requested one. No key was minted. Use a new principal_name for a scoped key, or mint an unscoped key on the existing principal explicitly.`);
|
|
134
|
+
}
|
|
135
|
+
if (existing.principal) {
|
|
136
|
+
const scopeRow = await dbAuth.principalEntity
|
|
137
|
+
.findFirst({
|
|
138
|
+
where: { principalId: { equalTo: existing.principal.id } },
|
|
139
|
+
select: { id: true },
|
|
140
|
+
})
|
|
141
|
+
.unwrap();
|
|
142
|
+
if (scopeRow.principalEntity || existing.principal?.isReadOnly) {
|
|
143
|
+
return fail(`Principal "${principalName}" already exists with a narrower scope (entity-scoped or read-only), so a key minted under it would not act as the signed-in user. No key was minted. Use a new principal_name for an unscoped key.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!principalId) {
|
|
147
|
+
// The SDK's PrincipalModel.create sends a nested {principal} input, but
|
|
148
|
+
// the live CreatePrincipalInput is flat and its payload only exposes
|
|
149
|
+
// `result: UUID` — raw GraphQL is the only working surface.
|
|
150
|
+
const created = await rawGraphql(authEndpoint, token.token, 'mutation ($input: CreatePrincipalInput!) { createPrincipal(input: $input) { result } }', {
|
|
151
|
+
input: {
|
|
152
|
+
name: principalName,
|
|
153
|
+
...(params.entity_ids?.length && { entityIds: params.entity_ids }),
|
|
154
|
+
...(params.read_only !== undefined && { isReadOnly: params.read_only }),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (created.errors?.length) {
|
|
158
|
+
return fail(`createPrincipal failed: ${created.errors.map((e) => e.message).join('; ')}`);
|
|
159
|
+
}
|
|
160
|
+
const payload = created.data?.createPrincipal;
|
|
161
|
+
principalId = payload?.result;
|
|
162
|
+
if (!principalId)
|
|
163
|
+
return fail('createPrincipal returned no principal id.');
|
|
164
|
+
}
|
|
165
|
+
const mint = () => dbAuth.mutation
|
|
166
|
+
.createApiKey({
|
|
167
|
+
input: {
|
|
168
|
+
principalId: principalId,
|
|
169
|
+
keyName,
|
|
170
|
+
...(params.read_only && { accessLevel: 'read_only' }),
|
|
171
|
+
expiresIn: { days: params.expires_in_days ?? 90 },
|
|
172
|
+
},
|
|
173
|
+
}, { select: { result: { select: { apiKey: true, keyId: true, expiresAt: true } } } })
|
|
174
|
+
.execute();
|
|
175
|
+
let minted = await mint();
|
|
176
|
+
if (!minted.ok && isStepUpError(minted.errors.map((e) => e.message))) {
|
|
177
|
+
if (!host.requestStepUp) {
|
|
178
|
+
return fail('Creating this key requires MFA step-up, and this host has no step-up flow. Complete step-up in the app (verify your password), then retry.');
|
|
179
|
+
}
|
|
180
|
+
const verified = await host.requestStepUp({ databaseId, databaseName, apiEndpoint });
|
|
181
|
+
if (!verified)
|
|
182
|
+
return fail('Step-up verification was not completed. No key was minted.');
|
|
183
|
+
minted = await mint();
|
|
184
|
+
}
|
|
185
|
+
if (!minted.ok) {
|
|
186
|
+
return fail(`createApiKey failed: ${minted.errors.map((e) => e.message).join('; ')}`);
|
|
187
|
+
}
|
|
188
|
+
const record = minted.data?.createApiKey?.result;
|
|
189
|
+
if (!record?.apiKey || !record.keyId)
|
|
190
|
+
return fail('createApiKey returned no key.');
|
|
191
|
+
const envVar = toEnvVar(keyName);
|
|
192
|
+
await host.deliverSecret({
|
|
193
|
+
databaseId,
|
|
194
|
+
cwd: ctx.cwd,
|
|
195
|
+
envVar,
|
|
196
|
+
plaintext: record.apiKey,
|
|
197
|
+
keyId: record.keyId,
|
|
198
|
+
expiresAt: record.expiresAt ?? undefined,
|
|
199
|
+
});
|
|
200
|
+
const scope = describeScope(params.entity_ids, params.read_only === true);
|
|
201
|
+
const message = `Created API key "${keyName}" (keyId: ${record.keyId}) for principal "${principalName}" — ${scope}. The key was written to .env as ${envVar} and revealed once to the user; it is not in this conversation. Reference it as ${envVar}.${record.expiresAt ? ` Expires ${record.expiresAt}.` : ''}`;
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: message }],
|
|
204
|
+
details: {
|
|
205
|
+
success: true,
|
|
206
|
+
message,
|
|
207
|
+
keyId: record.keyId,
|
|
208
|
+
envVar,
|
|
209
|
+
expiresAt: record.expiresAt ?? undefined,
|
|
210
|
+
principalId,
|
|
211
|
+
principalName,
|
|
212
|
+
scope,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return fail(err instanceof Error ? err.message : 'Failed to create the API key.');
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type HarnessTool } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const ParamZod: z.ZodObject<{}, z.core.$strip>;
|
|
4
|
+
type SchemaField = {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
isRequired: boolean;
|
|
8
|
+
defaultValue: string | null;
|
|
9
|
+
};
|
|
10
|
+
type SchemaTable = {
|
|
11
|
+
name: string;
|
|
12
|
+
fields: SchemaField[];
|
|
13
|
+
policies: string[];
|
|
14
|
+
};
|
|
15
|
+
export type DescribeSchemaDetails = {
|
|
16
|
+
tables: SchemaTable[];
|
|
17
|
+
};
|
|
18
|
+
export declare const describeSchemaTool: HarnessTool<typeof ParamZod, DescribeSchemaDetails>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeSchemaTool = void 0;
|
|
4
|
+
const harness_1 = require("@agentic-kit/harness");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const context_1 = require("../context");
|
|
7
|
+
const ParamZod = zod_1.z.object({});
|
|
8
|
+
function toDefaultValueString(value) {
|
|
9
|
+
if (value == null)
|
|
10
|
+
return null;
|
|
11
|
+
if (typeof value === 'string')
|
|
12
|
+
return value;
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
function formatText(details) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (details.tables.length === 0) {
|
|
18
|
+
lines.push('No application tables yet.');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
lines.push(`Tables (${details.tables.length}):`);
|
|
22
|
+
for (const table of details.tables) {
|
|
23
|
+
lines.push(` ${table.name}`);
|
|
24
|
+
for (const field of table.fields) {
|
|
25
|
+
const req = field.isRequired ? ' required' : '';
|
|
26
|
+
const def = field.defaultValue != null ? ` default=${field.defaultValue}` : '';
|
|
27
|
+
lines.push(` - ${field.name}: ${field.type}${req}${def}`);
|
|
28
|
+
}
|
|
29
|
+
if (table.policies.length > 0) {
|
|
30
|
+
lines.push(` policies: ${table.policies.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
exports.describeSchemaTool = {
|
|
37
|
+
name: 'describe_schema',
|
|
38
|
+
label: 'Describe schema',
|
|
39
|
+
description: 'Inspect the current Constructive database schema for this project: application tables with their fields (name, type, required, default). Read-only.',
|
|
40
|
+
promptSnippet: 'describe_schema: list the project database tables and their fields. Read-only — call before modifying schema.',
|
|
41
|
+
parameters: ParamZod,
|
|
42
|
+
async execute(_params, ctx) {
|
|
43
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
44
|
+
if (!resolved.context) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: resolved.reason }],
|
|
47
|
+
details: { tables: [] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const { api, databaseId } = resolved.context;
|
|
51
|
+
const tablesResult = await api.table
|
|
52
|
+
.findMany({
|
|
53
|
+
select: { id: true, name: true, category: true },
|
|
54
|
+
where: {
|
|
55
|
+
databaseId: { equalTo: databaseId },
|
|
56
|
+
or: [{ category: { equalTo: 'APP' } }, { name: { equalTo: 'users' } }],
|
|
57
|
+
},
|
|
58
|
+
orderBy: ['NAME_ASC'],
|
|
59
|
+
})
|
|
60
|
+
.execute();
|
|
61
|
+
if (!tablesResult.ok) {
|
|
62
|
+
const message = tablesResult.errors.map((e) => e.message).join('; ');
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: 'text', text: `Failed to read schema: ${message}` }],
|
|
65
|
+
details: { tables: [] },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const tableNodes = tablesResult.data.tables?.nodes ?? [];
|
|
69
|
+
const tableIds = tableNodes.map((t) => t.id);
|
|
70
|
+
const fieldsByTable = new Map();
|
|
71
|
+
const policiesByTable = new Map();
|
|
72
|
+
if (tableIds.length > 0) {
|
|
73
|
+
const [fieldsResult, policiesResult] = await Promise.all([
|
|
74
|
+
api.field
|
|
75
|
+
.findMany({
|
|
76
|
+
select: {
|
|
77
|
+
name: true,
|
|
78
|
+
type: true,
|
|
79
|
+
isRequired: true,
|
|
80
|
+
defaultValue: true,
|
|
81
|
+
tableId: true,
|
|
82
|
+
},
|
|
83
|
+
where: { tableId: { in: tableIds } },
|
|
84
|
+
})
|
|
85
|
+
.execute(),
|
|
86
|
+
api.policy
|
|
87
|
+
.findMany({
|
|
88
|
+
select: { tableId: true, policyType: true },
|
|
89
|
+
where: { tableId: { in: tableIds } },
|
|
90
|
+
})
|
|
91
|
+
.execute(),
|
|
92
|
+
]);
|
|
93
|
+
if (!fieldsResult.ok) {
|
|
94
|
+
const message = fieldsResult.errors.map((e) => e.message).join('; ');
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: `Failed to read fields: ${message}` }],
|
|
97
|
+
details: { tables: [] },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
for (const field of fieldsResult.data.fields?.nodes ?? []) {
|
|
101
|
+
const list = fieldsByTable.get(field.tableId) ?? [];
|
|
102
|
+
list.push({
|
|
103
|
+
name: field.name,
|
|
104
|
+
type: (0, harness_1.fieldTypeToTypeName)(field.type),
|
|
105
|
+
isRequired: Boolean(field.isRequired),
|
|
106
|
+
defaultValue: toDefaultValueString(field.defaultValue),
|
|
107
|
+
});
|
|
108
|
+
fieldsByTable.set(field.tableId, list);
|
|
109
|
+
}
|
|
110
|
+
// One policy spans CRUD as several rows; collapse to distinct types per table.
|
|
111
|
+
if (policiesResult.ok) {
|
|
112
|
+
for (const policy of policiesResult.data.policies?.nodes ?? []) {
|
|
113
|
+
if ((0, harness_1.isInternalPolicy)(policy.policyType))
|
|
114
|
+
continue;
|
|
115
|
+
const list = policiesByTable.get(policy.tableId) ?? [];
|
|
116
|
+
if (!list.includes(policy.policyType))
|
|
117
|
+
list.push(policy.policyType);
|
|
118
|
+
policiesByTable.set(policy.tableId, list);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const tables = tableNodes.map((table) => ({
|
|
123
|
+
name: table.name,
|
|
124
|
+
fields: fieldsByTable.get(table.id) ?? [],
|
|
125
|
+
policies: policiesByTable.get(table.id) ?? [],
|
|
126
|
+
}));
|
|
127
|
+
const details = { tables };
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: formatText(details) }],
|
|
130
|
+
details,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HarnessTool } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const ManageEntityTypesZod: z.ZodObject<{
|
|
4
|
+
action: z.ZodEnum<{
|
|
5
|
+
create: "create";
|
|
6
|
+
delete: "delete";
|
|
7
|
+
list: "list";
|
|
8
|
+
}>;
|
|
9
|
+
name: z.ZodOptional<z.ZodString>;
|
|
10
|
+
entity_type_id: z.ZodOptional<z.ZodString>;
|
|
11
|
+
description: z.ZodOptional<z.ZodString>;
|
|
12
|
+
prefix: z.ZodOptional<z.ZodString>;
|
|
13
|
+
parent_entity: z.ZodOptional<z.ZodString>;
|
|
14
|
+
is_visible: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
has_invites: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
has_profiles: z.ZodOptional<z.ZodBoolean>;
|
|
17
|
+
has_limits: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
has_levels: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
has_storage: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type EntityTypeSummary = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string | null;
|
|
24
|
+
description: string | null;
|
|
25
|
+
prefix: string | null;
|
|
26
|
+
parentEntity: string | null;
|
|
27
|
+
isVisible: boolean | null;
|
|
28
|
+
entityTableName: string | null;
|
|
29
|
+
installedModules: string[] | null;
|
|
30
|
+
};
|
|
31
|
+
export type ManageEntityTypesDetails = {
|
|
32
|
+
success: boolean;
|
|
33
|
+
message: string;
|
|
34
|
+
entityTypes?: EntityTypeSummary[];
|
|
35
|
+
};
|
|
36
|
+
export declare function defaultPrefix(name: string): string;
|
|
37
|
+
type Params = z.infer<typeof ManageEntityTypesZod>;
|
|
38
|
+
export declare function validateManageEntityTypes(params: Params): string | null;
|
|
39
|
+
export declare const manageEntityTypesTool: HarnessTool<typeof ManageEntityTypesZod, ManageEntityTypesDetails>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.manageEntityTypesTool = void 0;
|
|
4
|
+
exports.defaultPrefix = defaultPrefix;
|
|
5
|
+
exports.validateManageEntityTypes = validateManageEntityTypes;
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const context_1 = require("../context");
|
|
8
|
+
const ManageEntityTypesZod = zod_1.z.object({
|
|
9
|
+
action: zod_1.z
|
|
10
|
+
.enum(['list', 'create', 'delete'])
|
|
11
|
+
.describe('list: show the entity types provisioned in this database. create: provision a new entity type (creates its entity table + membership wiring). delete: remove the provision registration. Registrations are immutable once provisioned — there is no update; to change one, delete it and create a new one.'),
|
|
12
|
+
name: zod_1.z.string().describe('Entity type name. Required for create.').optional(),
|
|
13
|
+
entity_type_id: zod_1.z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('Entity type provision id. Required for delete — get it from action "list".')
|
|
16
|
+
.optional(),
|
|
17
|
+
description: zod_1.z
|
|
18
|
+
.string()
|
|
19
|
+
.describe('Human-readable description of the entity type. Create only.')
|
|
20
|
+
.optional(),
|
|
21
|
+
prefix: zod_1.z
|
|
22
|
+
.string()
|
|
23
|
+
.describe('Table-name prefix for the provisioned tables (e.g. "org" → org table, org_memberships). Create only; defaults to the snake_case of name.')
|
|
24
|
+
.optional(),
|
|
25
|
+
parent_entity: zod_1.z
|
|
26
|
+
.string()
|
|
27
|
+
.describe('Parent entity table name, for hierarchical types (e.g. a "team" under an "org"). Create only; the backend defaults it to the root entity (org).')
|
|
28
|
+
.optional(),
|
|
29
|
+
is_visible: zod_1.z
|
|
30
|
+
.boolean()
|
|
31
|
+
.describe('Whether the type shows up in end-user entity listings. Create only.')
|
|
32
|
+
.optional(),
|
|
33
|
+
has_invites: zod_1.z
|
|
34
|
+
.boolean()
|
|
35
|
+
.describe('Provision an invites module ({prefix}_invites + claimed invites). Create only.')
|
|
36
|
+
.optional(),
|
|
37
|
+
has_profiles: zod_1.z
|
|
38
|
+
.boolean()
|
|
39
|
+
.describe('Provision a profiles table for members of this type. Create only.')
|
|
40
|
+
.optional(),
|
|
41
|
+
has_limits: zod_1.z
|
|
42
|
+
.boolean()
|
|
43
|
+
.describe('Provision usage-limits tracking for this type. Create only.')
|
|
44
|
+
.optional(),
|
|
45
|
+
has_levels: zod_1.z
|
|
46
|
+
.boolean()
|
|
47
|
+
.describe('Provision levels/achievements support for this type. Create only.')
|
|
48
|
+
.optional(),
|
|
49
|
+
has_storage: zod_1.z
|
|
50
|
+
.boolean()
|
|
51
|
+
.describe('Provision a storage module (files + buckets tables) for this type. Create only.')
|
|
52
|
+
.optional(),
|
|
53
|
+
});
|
|
54
|
+
const SUMMARY_SELECT = {
|
|
55
|
+
id: true,
|
|
56
|
+
name: true,
|
|
57
|
+
description: true,
|
|
58
|
+
prefix: true,
|
|
59
|
+
parentEntity: true,
|
|
60
|
+
isVisible: true,
|
|
61
|
+
outEntityTableName: true,
|
|
62
|
+
outInstalledModules: true,
|
|
63
|
+
};
|
|
64
|
+
function toSummary(row) {
|
|
65
|
+
return {
|
|
66
|
+
id: row.id,
|
|
67
|
+
name: row.name,
|
|
68
|
+
description: row.description,
|
|
69
|
+
prefix: row.prefix,
|
|
70
|
+
parentEntity: row.parentEntity,
|
|
71
|
+
isVisible: row.isVisible,
|
|
72
|
+
entityTableName: row.outEntityTableName,
|
|
73
|
+
installedModules: row.outInstalledModules,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function formatSummary(s) {
|
|
77
|
+
const parts = [
|
|
78
|
+
`- ${s.name ?? '?'} (id: ${s.id})`,
|
|
79
|
+
` table: ${s.entityTableName ?? '?'}, prefix: ${s.prefix ?? '?'}`,
|
|
80
|
+
];
|
|
81
|
+
if (s.parentEntity)
|
|
82
|
+
parts.push(` parent: ${s.parentEntity}`);
|
|
83
|
+
if (s.description)
|
|
84
|
+
parts.push(` description: ${s.description}`);
|
|
85
|
+
if (s.installedModules?.length)
|
|
86
|
+
parts.push(` modules: ${s.installedModules.join(', ')}`);
|
|
87
|
+
return parts.join('\n');
|
|
88
|
+
}
|
|
89
|
+
function defaultPrefix(name) {
|
|
90
|
+
return name
|
|
91
|
+
.trim()
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
94
|
+
.replace(/^_+|_+$/g, '');
|
|
95
|
+
}
|
|
96
|
+
function validateManageEntityTypes(params) {
|
|
97
|
+
switch (params.action) {
|
|
98
|
+
case 'create':
|
|
99
|
+
if (!params.name?.trim())
|
|
100
|
+
return 'create requires "name".';
|
|
101
|
+
if (params.entity_type_id)
|
|
102
|
+
return 'create does not take "entity_type_id" — it mints a new one.';
|
|
103
|
+
return null;
|
|
104
|
+
case 'delete':
|
|
105
|
+
if (!params.entity_type_id)
|
|
106
|
+
return 'delete requires "entity_type_id" (use action "list" to find it).';
|
|
107
|
+
return null;
|
|
108
|
+
default:
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.manageEntityTypesTool = {
|
|
113
|
+
name: 'manage_entity_types',
|
|
114
|
+
label: 'Manage entity types',
|
|
115
|
+
description: 'List, create, or delete entity types in the project database. An entity type (e.g. organization, team, project) provisions its own entity table plus membership wiring, and is the unit that API keys can be scoped to. Registrations are immutable once provisioned — to change one, delete it and create a new one. Deleting removes the registration; the already-provisioned entity table stays in the API schema.',
|
|
116
|
+
promptSnippet: 'manage_entity_types: list/create/delete entity types (orgs, teams, …) in the project database. list is free; create/delete are gated.',
|
|
117
|
+
parameters: ManageEntityTypesZod,
|
|
118
|
+
async execute(params, ctx) {
|
|
119
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
120
|
+
if (!resolved.context) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: resolved.reason }],
|
|
123
|
+
details: { success: false, message: resolved.reason },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const invalid = validateManageEntityTypes(params);
|
|
127
|
+
if (invalid) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: invalid }],
|
|
130
|
+
details: { success: false, message: invalid },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const { modules, databaseId } = resolved.context;
|
|
134
|
+
try {
|
|
135
|
+
switch (params.action) {
|
|
136
|
+
case 'list': {
|
|
137
|
+
const result = await modules.entityTypeProvision
|
|
138
|
+
.findMany({
|
|
139
|
+
where: { databaseId: { equalTo: databaseId } },
|
|
140
|
+
select: SUMMARY_SELECT,
|
|
141
|
+
})
|
|
142
|
+
.unwrap();
|
|
143
|
+
const entityTypes = result.entityTypeProvisions.nodes.map((row) => toSummary(row));
|
|
144
|
+
const message = entityTypes.length === 0
|
|
145
|
+
? 'No entity types are provisioned in this database yet.'
|
|
146
|
+
: `${entityTypes.length} entity type${entityTypes.length === 1 ? '' : 's'}:\n${entityTypes.map(formatSummary).join('\n')}`;
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: message }],
|
|
149
|
+
details: { success: true, message, entityTypes },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
case 'create': {
|
|
153
|
+
const name = params.name.trim();
|
|
154
|
+
const result = await modules.entityTypeProvision
|
|
155
|
+
.create({
|
|
156
|
+
data: {
|
|
157
|
+
databaseId,
|
|
158
|
+
name,
|
|
159
|
+
prefix: params.prefix?.trim() || defaultPrefix(name),
|
|
160
|
+
...(params.description !== undefined && { description: params.description }),
|
|
161
|
+
...(params.parent_entity !== undefined && { parentEntity: params.parent_entity }),
|
|
162
|
+
...(params.is_visible !== undefined && { isVisible: params.is_visible }),
|
|
163
|
+
...(params.has_invites !== undefined && { hasInvites: params.has_invites }),
|
|
164
|
+
...(params.has_profiles !== undefined && { hasProfiles: params.has_profiles }),
|
|
165
|
+
...(params.has_limits !== undefined && { hasLimits: params.has_limits }),
|
|
166
|
+
...(params.has_levels !== undefined && { hasLevels: params.has_levels }),
|
|
167
|
+
...(params.has_storage && {
|
|
168
|
+
storage: [{}],
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
select: SUMMARY_SELECT,
|
|
172
|
+
})
|
|
173
|
+
.unwrap();
|
|
174
|
+
const row = result.createEntityTypeProvision?.entityTypeProvision;
|
|
175
|
+
if (!row)
|
|
176
|
+
throw new Error('createEntityTypeProvision returned no row.');
|
|
177
|
+
const summary = toSummary(row);
|
|
178
|
+
const message = `Created entity type "${summary.name}" (id: ${summary.id}) — entity table "${summary.entityTableName}"${summary.installedModules?.length ? `, modules: ${summary.installedModules.join(', ')}` : ''}. Run run_codegen to pull it into the typed SDK.`;
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: 'text', text: message }],
|
|
181
|
+
details: { success: true, message, entityTypes: [summary] },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case 'delete': {
|
|
185
|
+
const result = await modules.entityTypeProvision
|
|
186
|
+
.delete({
|
|
187
|
+
where: { id: params.entity_type_id },
|
|
188
|
+
select: { id: true, name: true, outEntityTableName: true },
|
|
189
|
+
})
|
|
190
|
+
.unwrap();
|
|
191
|
+
const row = result.deleteEntityTypeProvision?.entityTypeProvision;
|
|
192
|
+
const name = row?.name ?? params.entity_type_id;
|
|
193
|
+
const message = `Deleted entity type registration "${name}". Note: the provisioned entity table${row?.outEntityTableName ? ` "${row.outEntityTableName}"` : ''} remains in the API schema with its data.`;
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text: message }],
|
|
196
|
+
details: { success: true, message },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
const message = err instanceof Error ? err.message : 'Failed to manage entity types';
|
|
203
|
+
return { content: [{ type: 'text', text: message }], details: { success: false, message } };
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type HarnessTool } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export type MutationDetails = {
|
|
4
|
+
success: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
declare const DeleteTableZod: z.ZodObject<{
|
|
8
|
+
table_name: z.ZodString;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export declare const deleteTableTool: HarnessTool<typeof DeleteTableZod, MutationDetails>;
|
|
11
|
+
declare const CreateFieldZod: z.ZodObject<{
|
|
12
|
+
table_name: z.ZodString;
|
|
13
|
+
field_name: z.ZodString;
|
|
14
|
+
type: z.ZodString;
|
|
15
|
+
is_required: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
default_value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
17
|
+
is_unique: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const createFieldTool: HarnessTool<typeof CreateFieldZod, MutationDetails>;
|
|
20
|
+
declare const UpdateFieldZod: z.ZodObject<{
|
|
21
|
+
table_name: z.ZodString;
|
|
22
|
+
field_name: z.ZodString;
|
|
23
|
+
new_name: z.ZodOptional<z.ZodString>;
|
|
24
|
+
new_type: z.ZodOptional<z.ZodString>;
|
|
25
|
+
is_required: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
default_value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
27
|
+
is_unique: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export declare const updateFieldTool: HarnessTool<typeof UpdateFieldZod, MutationDetails>;
|
|
30
|
+
declare const DeleteFieldZod: z.ZodObject<{
|
|
31
|
+
table_name: z.ZodString;
|
|
32
|
+
field_name: z.ZodString;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
export declare const deleteFieldTool: HarnessTool<typeof DeleteFieldZod, MutationDetails>;
|
|
35
|
+
export {};
|