@agentic-kit/pi 0.9.2 → 0.10.1

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.
@@ -0,0 +1,203 @@
1
+ import { z } from 'zod';
2
+ import { resolveProjectContext } from '../context';
3
+ import { toolSchema } from '../tool-schema';
4
+ const ManageEntityTypesZod = z.object({
5
+ action: z
6
+ .enum(['list', 'create', 'delete'])
7
+ .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.'),
8
+ name: z.string().describe('Entity type name. Required for create.').optional(),
9
+ entity_type_id: z
10
+ .string()
11
+ .describe('Entity type provision id. Required for delete — get it from action "list".')
12
+ .optional(),
13
+ description: z
14
+ .string()
15
+ .describe('Human-readable description of the entity type. Create only.')
16
+ .optional(),
17
+ prefix: z
18
+ .string()
19
+ .describe('Table-name prefix for the provisioned tables (e.g. "org" → org table, org_memberships). Create only; defaults to the snake_case of name.')
20
+ .optional(),
21
+ parent_entity: z
22
+ .string()
23
+ .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).')
24
+ .optional(),
25
+ is_visible: z
26
+ .boolean()
27
+ .describe('Whether the type shows up in end-user entity listings. Create only.')
28
+ .optional(),
29
+ has_invites: z
30
+ .boolean()
31
+ .describe('Provision an invites module ({prefix}_invites + claimed invites). Create only.')
32
+ .optional(),
33
+ has_profiles: z
34
+ .boolean()
35
+ .describe('Provision a profiles table for members of this type. Create only.')
36
+ .optional(),
37
+ has_limits: z
38
+ .boolean()
39
+ .describe('Provision usage-limits tracking for this type. Create only.')
40
+ .optional(),
41
+ has_levels: z
42
+ .boolean()
43
+ .describe('Provision levels/achievements support for this type. Create only.')
44
+ .optional(),
45
+ has_storage: z
46
+ .boolean()
47
+ .describe('Provision a storage module (files + buckets tables) for this type. Create only.')
48
+ .optional(),
49
+ });
50
+ const ManageEntityTypesSchema = toolSchema(ManageEntityTypesZod);
51
+ const SUMMARY_SELECT = {
52
+ id: true,
53
+ name: true,
54
+ description: true,
55
+ prefix: true,
56
+ parentEntity: true,
57
+ isVisible: true,
58
+ outEntityTableName: true,
59
+ outInstalledModules: true,
60
+ };
61
+ function toSummary(row) {
62
+ return {
63
+ id: row.id,
64
+ name: row.name,
65
+ description: row.description,
66
+ prefix: row.prefix,
67
+ parentEntity: row.parentEntity,
68
+ isVisible: row.isVisible,
69
+ entityTableName: row.outEntityTableName,
70
+ installedModules: row.outInstalledModules,
71
+ };
72
+ }
73
+ function formatSummary(s) {
74
+ const parts = [
75
+ `- ${s.name ?? '?'} (id: ${s.id})`,
76
+ ` table: ${s.entityTableName ?? '?'}, prefix: ${s.prefix ?? '?'}`,
77
+ ];
78
+ if (s.parentEntity)
79
+ parts.push(` parent: ${s.parentEntity}`);
80
+ if (s.description)
81
+ parts.push(` description: ${s.description}`);
82
+ if (s.installedModules?.length)
83
+ parts.push(` modules: ${s.installedModules.join(', ')}`);
84
+ return parts.join('\n');
85
+ }
86
+ export function defaultPrefix(name) {
87
+ return name
88
+ .trim()
89
+ .toLowerCase()
90
+ .replace(/[^a-z0-9]+/g, '_')
91
+ .replace(/^_+|_+$/g, '');
92
+ }
93
+ export function validateManageEntityTypes(params) {
94
+ switch (params.action) {
95
+ case 'create':
96
+ if (!params.name?.trim())
97
+ return 'create requires "name".';
98
+ if (params.entity_type_id)
99
+ return 'create does not take "entity_type_id" — it mints a new one.';
100
+ return null;
101
+ case 'delete':
102
+ if (!params.entity_type_id)
103
+ return 'delete requires "entity_type_id" (use action "list" to find it).';
104
+ return null;
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+ export const manageEntityTypesTool = {
110
+ name: 'manage_entity_types',
111
+ label: 'Manage entity types',
112
+ 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.',
113
+ promptSnippet: 'manage_entity_types: list/create/delete entity types (orgs, teams, …) in the project database. list is free; create/delete are gated.',
114
+ parameters: ManageEntityTypesSchema,
115
+ async execute(_id, params, _signal, _onUpdate, ctx) {
116
+ const resolved = await resolveProjectContext(ctx.cwd);
117
+ if (!resolved.context) {
118
+ return {
119
+ content: [{ type: 'text', text: resolved.reason }],
120
+ details: { success: false, message: resolved.reason },
121
+ };
122
+ }
123
+ const invalid = validateManageEntityTypes(params);
124
+ if (invalid) {
125
+ return {
126
+ content: [{ type: 'text', text: invalid }],
127
+ details: { success: false, message: invalid },
128
+ };
129
+ }
130
+ const { modules, databaseId } = resolved.context;
131
+ try {
132
+ switch (params.action) {
133
+ case 'list': {
134
+ const result = await modules.entityTypeProvision
135
+ .findMany({
136
+ where: { databaseId: { equalTo: databaseId } },
137
+ select: SUMMARY_SELECT,
138
+ })
139
+ .unwrap();
140
+ const entityTypes = result.entityTypeProvisions.nodes.map((row) => toSummary(row));
141
+ const message = entityTypes.length === 0
142
+ ? 'No entity types are provisioned in this database yet.'
143
+ : `${entityTypes.length} entity type${entityTypes.length === 1 ? '' : 's'}:\n${entityTypes.map(formatSummary).join('\n')}`;
144
+ return {
145
+ content: [{ type: 'text', text: message }],
146
+ details: { success: true, message, entityTypes },
147
+ };
148
+ }
149
+ case 'create': {
150
+ const name = params.name.trim();
151
+ const result = await modules.entityTypeProvision
152
+ .create({
153
+ data: {
154
+ databaseId,
155
+ name,
156
+ prefix: params.prefix?.trim() || defaultPrefix(name),
157
+ ...(params.description !== undefined && { description: params.description }),
158
+ ...(params.parent_entity !== undefined && { parentEntity: params.parent_entity }),
159
+ ...(params.is_visible !== undefined && { isVisible: params.is_visible }),
160
+ ...(params.has_invites !== undefined && { hasInvites: params.has_invites }),
161
+ ...(params.has_profiles !== undefined && { hasProfiles: params.has_profiles }),
162
+ ...(params.has_limits !== undefined && { hasLimits: params.has_limits }),
163
+ ...(params.has_levels !== undefined && { hasLevels: params.has_levels }),
164
+ ...(params.has_storage && {
165
+ storage: [{}],
166
+ }),
167
+ },
168
+ select: SUMMARY_SELECT,
169
+ })
170
+ .unwrap();
171
+ const row = result.createEntityTypeProvision?.entityTypeProvision;
172
+ if (!row)
173
+ throw new Error('createEntityTypeProvision returned no row.');
174
+ const summary = toSummary(row);
175
+ 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.`;
176
+ return {
177
+ content: [{ type: 'text', text: message }],
178
+ details: { success: true, message, entityTypes: [summary] },
179
+ };
180
+ }
181
+ case 'delete': {
182
+ const result = await modules.entityTypeProvision
183
+ .delete({
184
+ where: { id: params.entity_type_id },
185
+ select: { id: true, name: true, outEntityTableName: true },
186
+ })
187
+ .unwrap();
188
+ const row = result.deleteEntityTypeProvision?.entityTypeProvision;
189
+ const name = row?.name ?? params.entity_type_id;
190
+ const message = `Deleted entity type registration "${name}". Note: the provisioned entity table${row?.outEntityTableName ? ` "${row.outEntityTableName}"` : ''} remains in the API schema with its data.`;
191
+ return {
192
+ content: [{ type: 'text', text: message }],
193
+ details: { success: true, message },
194
+ };
195
+ }
196
+ }
197
+ }
198
+ catch (err) {
199
+ const message = err instanceof Error ? err.message : 'Failed to manage entity types';
200
+ return { content: [{ type: 'text', text: message }], details: { success: false, message } };
201
+ }
202
+ },
203
+ };
@@ -109,7 +109,7 @@ export const provisionDatabaseTool = {
109
109
  // with no usable bearer errors out here rather than silently minting a
110
110
  // throwaway owner. Gate BEFORE prewarm so an error return doesn't leak a
111
111
  // detached background scaffold/install.
112
- const credential = selectProvisionCredential(host.account());
112
+ const credential = selectProvisionCredential(host.account(), host.signInHint);
113
113
  if (credential.mode === 'error') {
114
114
  return fail(`Cannot provision a database: ${credential.reason}`);
115
115
  }
@@ -129,6 +129,7 @@ export const provisionDatabaseTool = {
129
129
  endpoint: apiEndpoint,
130
130
  bearer: credential.bearer,
131
131
  databaseId: existingEnv.DATABASE_ID,
132
+ signInHint: host.signInHint,
132
133
  });
133
134
  if (probe.outcome === 'unreachable') {
134
135
  return fail(`Cannot verify the existing database binding: backend unreachable (${probe.detail}). Check the Constructive backend is running, then retry — do not reprovision on an unreachable backend.`);
package/host.d.ts CHANGED
@@ -48,6 +48,29 @@ export interface HostProvisionOverlay {
48
48
  add?: (string | [string, Record<string, unknown>])[];
49
49
  remove?: string[];
50
50
  }
51
+ /**
52
+ * A minted secret handed to the host for out-of-band delivery (.env write +
53
+ * one-time reveal). The plaintext never enters tool results or the transcript;
54
+ * pi forgets it after this call.
55
+ */
56
+ export type SecretDelivery = {
57
+ databaseId: string;
58
+ /** Project directory whose `.env` receives the key. */
59
+ cwd: string;
60
+ envVar: string;
61
+ plaintext: string;
62
+ keyId: string;
63
+ expiresAt?: string;
64
+ };
65
+ /**
66
+ * Context for a host-side step-up: enough to derive the per-database auth
67
+ * endpoint and look up the app session without re-resolving the project.
68
+ */
69
+ export type StepUpRequest = {
70
+ databaseId: string;
71
+ databaseName: string;
72
+ apiEndpoint: string;
73
+ };
51
74
  export interface PiToolsHost {
52
75
  /** Signed-in platform account, or null/undefined when signed out. */
53
76
  account(): HostAccount | null | undefined;
@@ -55,6 +78,12 @@ export interface PiToolsHost {
55
78
  backendConfig(): HostBackendConfig | null | undefined;
56
79
  /** Optional data-plane token broker (see DataAuthBroker). */
57
80
  dataAuthBroker?: DataAuthBroker;
81
+ /**
82
+ * Host-specific sign-in instruction, substituted into signed-out failure
83
+ * reasons (e.g. the CLI's "Run `agent login` to sign in."). Absent hosts get
84
+ * the desktop wording.
85
+ */
86
+ signInHint?: string;
58
87
  /** Harvest an end-user token from the host's app preview, if it has one. */
59
88
  previewToken?(): Promise<PreviewToken | null>;
60
89
  /** Treat tokens expiring within this window as already expired. Default 30s. */
@@ -66,6 +95,17 @@ export interface PiToolsHost {
66
95
  * (e.g. materialized from appstash / a pinned git ref), never as code.
67
96
  */
68
97
  provisionOverlay?(): HostProvisionOverlay | null | undefined | Promise<HostProvisionOverlay | null | undefined>;
98
+ /**
99
+ * Complete MFA step-up for the database's app session in the host's own
100
+ * process (password dialog + verifyPassword). The password never passes
101
+ * through pi or the model. Resolve true when step-up succeeded.
102
+ */
103
+ requestStepUp?(request: StepUpRequest): Promise<boolean>;
104
+ /**
105
+ * Deliver a minted secret to the user (.env write + one-time reveal).
106
+ * Required for create_api_key — without it the tool refuses to mint.
107
+ */
108
+ deliverSecret?(delivery: SecretDelivery): Promise<void>;
69
109
  }
70
110
  export declare const DEFAULT_DATA_TOKEN_SKEW_MS = 30000;
71
111
  export declare function configureHost(host: PiToolsHost): void;
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export declare const dbTools: ExtensionFactory;
4
4
  /** Configure the host and get the extension in one call. */
5
5
  export declare function createDbTools(host: PiToolsHost): ExtensionFactory;
6
6
  export { type ConfirmGate, type ConfirmGateDeps, createConfirmGate } from './confirm-gate';
7
- export { type ModulesClient, type ProjectContext, resolveDataToken, resolveProjectContext, } from './context';
7
+ export { deriveSubdomainEndpoint, type ModulesClient, type ProjectContext, resolveDataToken, resolveProjectContext, } from './context';
8
8
  export { type ActiveDataToken, configureHost, type DataAuthBroker, getHost, type HostAccount, type HostBackendConfig, type HostProvisionOverlay, type PiToolsHost, type PreviewToken, } from './host';
9
9
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, type ProvisionManifest, } from './provision-database/manifest';
10
10
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, type ModulePreset, type ProvisionModule, } from './provision-database/presets';
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createTemplatePreviewTables = exports.toolSchema = exports.resolveProvisionModules = exports.moduleKey = exports.getModulePreset = exports.DEFAULT_PROVISION_PRESET = exports.allModulePresets = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.loadProvisionManifest = exports.getHost = exports.configureHost = exports.resolveProjectContext = exports.resolveDataToken = exports.createConfirmGate = exports.dbTools = void 0;
3
+ exports.createTemplatePreviewTables = exports.toolSchema = exports.resolveProvisionModules = exports.moduleKey = exports.getModulePreset = exports.DEFAULT_PROVISION_PRESET = exports.allModulePresets = exports.PROVISION_MANIFEST_FILE = exports.parseProvisionManifest = exports.loadProvisionManifest = exports.getHost = exports.configureHost = exports.resolveProjectContext = exports.resolveDataToken = exports.deriveSubdomainEndpoint = exports.createConfirmGate = exports.dbTools = void 0;
4
4
  exports.createDbTools = createDbTools;
5
5
  const confirm_gate_1 = require("./confirm-gate");
6
6
  const context_1 = require("./context");
@@ -8,7 +8,9 @@ const host_1 = require("./host");
8
8
  const add_policies_1 = require("./tools/add-policies");
9
9
  const add_records_1 = require("./tools/add-records");
10
10
  const add_relation_1 = require("./tools/add-relation");
11
+ const create_api_key_1 = require("./tools/create-api-key");
11
12
  const describe_schema_1 = require("./tools/describe-schema");
13
+ const manage_entity_types_1 = require("./tools/manage-entity-types");
12
14
  const mutations_1 = require("./tools/mutations");
13
15
  const provision_blueprint_1 = require("./tools/provision-blueprint");
14
16
  const provision_database_1 = require("./tools/provision-database");
@@ -30,6 +32,8 @@ const dbTools = (pi) => {
30
32
  pi.registerTool(templates_1.updateTemplateTool);
31
33
  pi.registerTool(templates_1.deleteTemplateTool);
32
34
  pi.registerTool(add_records_1.addRecordsTool);
35
+ pi.registerTool(manage_entity_types_1.manageEntityTypesTool);
36
+ pi.registerTool(create_api_key_1.createApiKeyTool);
33
37
  pi.registerTool(run_codegen_1.runCodegenTool);
34
38
  const gate = (0, confirm_gate_1.createConfirmGate)({
35
39
  resolveProjectContext: context_1.resolveProjectContext,
@@ -48,6 +52,7 @@ function createDbTools(host) {
48
52
  var confirm_gate_2 = require("./confirm-gate");
49
53
  Object.defineProperty(exports, "createConfirmGate", { enumerable: true, get: function () { return confirm_gate_2.createConfirmGate; } });
50
54
  var context_2 = require("./context");
55
+ Object.defineProperty(exports, "deriveSubdomainEndpoint", { enumerable: true, get: function () { return context_2.deriveSubdomainEndpoint; } });
51
56
  Object.defineProperty(exports, "resolveDataToken", { enumerable: true, get: function () { return context_2.resolveDataToken; } });
52
57
  Object.defineProperty(exports, "resolveProjectContext", { enumerable: true, get: function () { return context_2.resolveProjectContext; } });
53
58
  var host_2 = require("./host");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-kit/pi",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
5
  "description": "Pi adapter for agentic-kit — the Constructive typed db tools (provision, blueprint, codegen, records, policies, templates) and confirm gate as a pi coding-agent extension",
6
6
  "main": "index.js",
@@ -29,9 +29,9 @@
29
29
  "test:watch": "jest --watch"
30
30
  },
31
31
  "dependencies": {
32
- "@agentic-kit/harness": "^0.9.0",
33
- "@constructive-io/graphql-query": "^4.12.2",
34
- "@constructive-io/sdk": "^1.11.2",
32
+ "@agentic-kit/harness": "^0.10.1",
33
+ "@constructive-io/graphql-query": "^4.12.4",
34
+ "@constructive-io/sdk": "^1.11.4",
35
35
  "node-type-registry": "^1.14.1",
36
36
  "zod": "^4.4.3"
37
37
  },
@@ -48,5 +48,5 @@
48
48
  "coding-agent",
49
49
  "constructive"
50
50
  ],
51
- "gitHead": "c9dcb0c96174dfb0bc6fd63e46cc7776ea7af173"
51
+ "gitHead": "0d9f0718a8c768cc8b7659525953dc762833bd6b"
52
52
  }
@@ -11,4 +11,4 @@ export type ProvisionCredential = {
11
11
  mode: 'error';
12
12
  reason: string;
13
13
  };
14
- export declare function selectProvisionCredential(account: AccountCredential | null | undefined): ProvisionCredential;
14
+ export declare function selectProvisionCredential(account: AccountCredential | null | undefined, signInHint?: string): ProvisionCredential;
@@ -10,13 +10,13 @@
10
10
  // Side-effect-free so the branch is unit-testable without runtime/network.
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.selectProvisionCredential = selectProvisionCredential;
13
- function selectProvisionCredential(account) {
13
+ function selectProvisionCredential(account, signInHint) {
14
14
  const bearer = account?.apiKey ?? account?.accessToken;
15
15
  if (account?.userId && bearer) {
16
16
  return { mode: 'account', ownerId: account.userId, bearer };
17
17
  }
18
18
  return {
19
19
  mode: 'error',
20
- reason: 'No usable account credential. Sign in (or unlock your session) before provisioning a database.',
20
+ reason: `No usable account credential. ${signInHint ?? 'Sign in (or unlock your session) before provisioning a database.'}`,
21
21
  };
22
22
  }
@@ -0,0 +1,18 @@
1
+ import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
2
+ declare const CreateApiKeySchema: import("typebox/type").TSchema;
3
+ export type CreateApiKeyDetails = {
4
+ success: boolean;
5
+ message: string;
6
+ keyId?: string;
7
+ envVar?: string;
8
+ expiresAt?: string;
9
+ principalId?: string;
10
+ principalName?: string;
11
+ scope?: string;
12
+ needsAuth?: boolean;
13
+ };
14
+ export declare function toEnvVar(keyName: string): string;
15
+ export declare function isStepUpError(messages: string[]): boolean;
16
+ export declare function describeScope(entityIds: string[] | undefined, readOnly: boolean): string;
17
+ export declare const createApiKeyTool: ToolDefinition<typeof CreateApiKeySchema, CreateApiKeyDetails>;
18
+ export {};
@@ -0,0 +1,222 @@
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 tool_schema_1 = require("../tool-schema");
12
+ const CreateApiKeyZod = zod_1.z.object({
13
+ key_name: zod_1.z
14
+ .string()
15
+ .describe('Name for the API key (e.g. "deploy bot"). The .env variable name derives from it (UPPER_SNAKE + _API_KEY).'),
16
+ principal_name: zod_1.z
17
+ .string()
18
+ .describe('Machine identity (principal) that owns the key. Defaults to key_name. An existing principal with this name is reused for unscoped keys.')
19
+ .optional(),
20
+ entity_ids: zod_1.z
21
+ .array(zod_1.z.string())
22
+ .describe('Entity row UUIDs to scope the principal to. Omit for a personal (unscoped) key that acts as the signed-in app user.')
23
+ .optional(),
24
+ read_only: zod_1.z
25
+ .boolean()
26
+ .describe('Restrict the principal and key to read-only operations.')
27
+ .optional(),
28
+ expires_in_days: zod_1.z
29
+ .number()
30
+ .int()
31
+ .positive()
32
+ .describe('Key lifetime in days. Default 90.')
33
+ .optional(),
34
+ });
35
+ const CreateApiKeySchema = (0, tool_schema_1.toolSchema)(CreateApiKeyZod);
36
+ function fail(message, extra) {
37
+ return {
38
+ content: [{ type: 'text', text: message }],
39
+ details: { success: false, message, ...extra },
40
+ };
41
+ }
42
+ function toEnvVar(keyName) {
43
+ const base = keyName
44
+ .trim()
45
+ .toUpperCase()
46
+ .replace(/[^A-Z0-9]+/g, '_')
47
+ .replace(/^_+|_+$/g, '');
48
+ return base.endsWith('_API_KEY') ? base : `${base}_API_KEY`;
49
+ }
50
+ function isStepUpError(messages) {
51
+ return messages.some((m) => /step[\s_-]?up/i.test(m));
52
+ }
53
+ function describeScope(entityIds, readOnly) {
54
+ const parts = [];
55
+ parts.push(entityIds?.length
56
+ ? `scoped to ${entityIds.length} entit${entityIds.length === 1 ? 'y' : 'ies'}`
57
+ : 'unscoped (acts as the signed-in app user)');
58
+ if (readOnly)
59
+ parts.push('read-only');
60
+ return parts.join(', ');
61
+ }
62
+ async function rawGraphql(endpoint, token, query, variables) {
63
+ const res = await fetch(endpoint, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
68
+ },
69
+ body: JSON.stringify({ query, variables }),
70
+ });
71
+ return (await res.json());
72
+ }
73
+ async function createPrincipalInputFields(endpoint) {
74
+ const probe = await rawGraphql(endpoint, undefined, '{ __type(name: "CreatePrincipalInput") { inputFields { name } } }');
75
+ const type = probe.data?.__type;
76
+ return new Set((type?.inputFields ?? []).map((f) => f.name));
77
+ }
78
+ exports.createApiKeyTool = {
79
+ name: 'create_api_key',
80
+ label: 'Create API key',
81
+ 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.',
82
+ 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.',
83
+ parameters: CreateApiKeySchema,
84
+ async execute(_id, params, _signal, _onUpdate, ctx) {
85
+ const keyName = params.key_name?.trim();
86
+ if (!keyName)
87
+ return fail('create_api_key requires "key_name".');
88
+ const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd, { plane: 'data' });
89
+ if (!resolved.context)
90
+ return fail(resolved.reason);
91
+ const { databaseId, databaseName, apiEndpoint } = resolved.context;
92
+ if (!databaseName) {
93
+ return fail('Cannot resolve the app auth endpoint (DATABASE_NAME missing from .env). Re-provision the database, then retry.');
94
+ }
95
+ const host = (0, host_1.getHost)();
96
+ if (!host.deliverSecret) {
97
+ 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.');
98
+ }
99
+ const token = await (0, context_1.resolveDataToken)(resolved.context);
100
+ if (!token.token) {
101
+ return fail(token.reason ?? 'Sign in to the app database to create an API key.', {
102
+ needsAuth: true,
103
+ });
104
+ }
105
+ const authEndpoint = (0, context_1.deriveSubdomainEndpoint)(apiEndpoint, `auth-${databaseName}`);
106
+ if (!authEndpoint)
107
+ return fail('Cannot derive the app auth endpoint.');
108
+ const scoped = Boolean(params.entity_ids?.length) || params.read_only === true;
109
+ try {
110
+ if (scoped) {
111
+ const fields = await createPrincipalInputFields(authEndpoint);
112
+ const missing = [
113
+ ...(params.entity_ids?.length && !fields.has('entityIds') ? ['entityIds'] : []),
114
+ ...(params.read_only === true && !fields.has('isReadOnly') ? ['isReadOnly'] : []),
115
+ ];
116
+ if (missing.length) {
117
+ 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.`);
118
+ }
119
+ }
120
+ const dbAuth = sdk_1.auth.createClient({
121
+ endpoint: authEndpoint,
122
+ headers: { Authorization: `Bearer ${token.token}` },
123
+ });
124
+ const principalName = params.principal_name?.trim() || keyName;
125
+ const existing = await dbAuth.principal
126
+ .findFirst({
127
+ where: { name: { equalTo: principalName } },
128
+ select: { id: true, name: true, userId: true, isReadOnly: true },
129
+ })
130
+ .unwrap();
131
+ // createApiKey (like createPrincipal.result) takes the principal's
132
+ // identity userId; principal_entity.principalId references the row id.
133
+ let principalId = existing.principal?.userId;
134
+ if (existing.principal && scoped) {
135
+ 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.`);
136
+ }
137
+ if (existing.principal) {
138
+ const scopeRow = await dbAuth.principalEntity
139
+ .findFirst({
140
+ where: { principalId: { equalTo: existing.principal.id } },
141
+ select: { id: true },
142
+ })
143
+ .unwrap();
144
+ if (scopeRow.principalEntity || existing.principal?.isReadOnly) {
145
+ 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.`);
146
+ }
147
+ }
148
+ if (!principalId) {
149
+ // The SDK's PrincipalModel.create sends a nested {principal} input, but
150
+ // the live CreatePrincipalInput is flat and its payload only exposes
151
+ // `result: UUID` — raw GraphQL is the only working surface.
152
+ const created = await rawGraphql(authEndpoint, token.token, 'mutation ($input: CreatePrincipalInput!) { createPrincipal(input: $input) { result } }', {
153
+ input: {
154
+ name: principalName,
155
+ ...(params.entity_ids?.length && { entityIds: params.entity_ids }),
156
+ ...(params.read_only !== undefined && { isReadOnly: params.read_only }),
157
+ },
158
+ });
159
+ if (created.errors?.length) {
160
+ return fail(`createPrincipal failed: ${created.errors.map((e) => e.message).join('; ')}`);
161
+ }
162
+ const payload = created.data?.createPrincipal;
163
+ principalId = payload?.result;
164
+ if (!principalId)
165
+ return fail('createPrincipal returned no principal id.');
166
+ }
167
+ const mint = () => dbAuth.mutation
168
+ .createApiKey({
169
+ input: {
170
+ principalId: principalId,
171
+ keyName,
172
+ ...(params.read_only && { accessLevel: 'read_only' }),
173
+ expiresIn: { days: params.expires_in_days ?? 90 },
174
+ },
175
+ }, { select: { result: { select: { apiKey: true, keyId: true, expiresAt: true } } } })
176
+ .execute();
177
+ let minted = await mint();
178
+ if (!minted.ok && isStepUpError(minted.errors.map((e) => e.message))) {
179
+ if (!host.requestStepUp) {
180
+ 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.');
181
+ }
182
+ const verified = await host.requestStepUp({ databaseId, databaseName, apiEndpoint });
183
+ if (!verified)
184
+ return fail('Step-up verification was not completed. No key was minted.');
185
+ minted = await mint();
186
+ }
187
+ if (!minted.ok) {
188
+ return fail(`createApiKey failed: ${minted.errors.map((e) => e.message).join('; ')}`);
189
+ }
190
+ const record = minted.data?.createApiKey?.result;
191
+ if (!record?.apiKey || !record.keyId)
192
+ return fail('createApiKey returned no key.');
193
+ const envVar = toEnvVar(keyName);
194
+ await host.deliverSecret({
195
+ databaseId,
196
+ cwd: ctx.cwd,
197
+ envVar,
198
+ plaintext: record.apiKey,
199
+ keyId: record.keyId,
200
+ expiresAt: record.expiresAt ?? undefined,
201
+ });
202
+ const scope = describeScope(params.entity_ids, params.read_only === true);
203
+ 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}.` : ''}`;
204
+ return {
205
+ content: [{ type: 'text', text: message }],
206
+ details: {
207
+ success: true,
208
+ message,
209
+ keyId: record.keyId,
210
+ envVar,
211
+ expiresAt: record.expiresAt ?? undefined,
212
+ principalId,
213
+ principalName,
214
+ scope,
215
+ },
216
+ };
217
+ }
218
+ catch (err) {
219
+ return fail(err instanceof Error ? err.message : 'Failed to create the API key.');
220
+ }
221
+ },
222
+ };
@@ -0,0 +1,41 @@
1
+ import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
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
+ declare const ManageEntityTypesSchema: import("typebox/type").TSchema;
22
+ export type EntityTypeSummary = {
23
+ id: string;
24
+ name: string | null;
25
+ description: string | null;
26
+ prefix: string | null;
27
+ parentEntity: string | null;
28
+ isVisible: boolean | null;
29
+ entityTableName: string | null;
30
+ installedModules: string[] | null;
31
+ };
32
+ export type ManageEntityTypesDetails = {
33
+ success: boolean;
34
+ message: string;
35
+ entityTypes?: EntityTypeSummary[];
36
+ };
37
+ export declare function defaultPrefix(name: string): string;
38
+ type Params = z.infer<typeof ManageEntityTypesZod>;
39
+ export declare function validateManageEntityTypes(params: Params): string | null;
40
+ export declare const manageEntityTypesTool: ToolDefinition<typeof ManageEntityTypesSchema, ManageEntityTypesDetails>;
41
+ export {};