@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.
package/context.js CHANGED
@@ -75,7 +75,7 @@ async function resolveProjectContext(cwd, options = {}) {
75
75
  if (!accountBearer) {
76
76
  return {
77
77
  context: null,
78
- reason: 'No usable account credential to reach the Constructive control plane. Sign in to the app, then retry.',
78
+ reason: `No usable account credential to reach the Constructive control plane. ${host.signInHint ?? 'Sign in to the app, then retry.'}`,
79
79
  code: 'missing-credentials',
80
80
  };
81
81
  }
@@ -88,6 +88,7 @@ async function resolveProjectContext(cwd, options = {}) {
88
88
  endpoint: controlApiEndpoint,
89
89
  bearer: accountBearer,
90
90
  databaseId,
91
+ signInHint: host.signInHint,
91
92
  });
92
93
  if (probe.outcome === 'unreachable') {
93
94
  return {
@@ -149,7 +150,8 @@ async function resolveProjectContext(cwd, options = {}) {
149
150
  reason: '',
150
151
  };
151
152
  }
152
- const NEEDS_AUTH_REASON = 'Not signed in to the app database yet. Sign in from the Sheets tab, or open the Preview and sign in to your app, then try again.';
153
+ const needsAuthReason = () => `Not signed in to the app database yet. ${(0, host_1.getHost)().signInHint ??
154
+ 'Sign in from the Sheets tab, or open the Preview and sign in to your app, then try again.'}`;
153
155
  // Resolve a data-plane token for record operations: serve the broker's active
154
156
  // account while valid, otherwise harvest the token the user created by signing
155
157
  // into their app in the Preview and adopt it into the broker. Also returns the
@@ -164,15 +166,15 @@ async function resolveDataToken(context) {
164
166
  return { token: active.token, userId: active.userId };
165
167
  const preview = host.previewToken ? await host.previewToken() : null;
166
168
  if (!preview)
167
- return { reason: NEEDS_AUTH_REASON };
169
+ return { reason: needsAuthReason() };
168
170
  if (broker?.isInvalidToken(context.databaseId, preview.accessToken)) {
169
- return { reason: NEEDS_AUTH_REASON };
171
+ return { reason: needsAuthReason() };
170
172
  }
171
173
  const parsed = preview.accessTokenExpiresAt ? Date.parse(preview.accessTokenExpiresAt) : NaN;
172
174
  const expiresAt = Number.isNaN(parsed) ? Date.now() + 60 * 60 * 1000 : parsed;
173
175
  const skewMs = host.dataTokenSkewMs ?? host_1.DEFAULT_DATA_TOKEN_SKEW_MS;
174
176
  if (expiresAt <= Date.now() + skewMs)
175
- return { reason: NEEDS_AUTH_REASON };
177
+ return { reason: needsAuthReason() };
176
178
  broker?.adoptToken(context.databaseId, {
177
179
  userId: preview.userId,
178
180
  token: preview.accessToken,
package/db-probe.d.ts CHANGED
@@ -26,4 +26,5 @@ export declare function probeDatabase(args: {
26
26
  bearer: string;
27
27
  databaseId: string;
28
28
  executor?: ProbeExecutor;
29
+ signInHint?: string;
29
30
  }): Promise<DatabaseProbe>;
package/db-probe.js CHANGED
@@ -39,7 +39,7 @@ async function probeDatabase(args) {
39
39
  if (AUTH_ERROR_RE.test(detail)) {
40
40
  return {
41
41
  outcome: 'unreachable',
42
- detail: `${detail} — the account credential was rejected; sign in again, then retry`,
42
+ detail: `${detail} — the account credential was rejected. ${args.signInHint ?? 'Sign in again, then retry.'}`,
43
43
  };
44
44
  }
45
45
  return { outcome: 'missing' };
package/esm/context.js CHANGED
@@ -66,7 +66,7 @@ export async function resolveProjectContext(cwd, options = {}) {
66
66
  if (!accountBearer) {
67
67
  return {
68
68
  context: null,
69
- reason: 'No usable account credential to reach the Constructive control plane. Sign in to the app, then retry.',
69
+ reason: `No usable account credential to reach the Constructive control plane. ${host.signInHint ?? 'Sign in to the app, then retry.'}`,
70
70
  code: 'missing-credentials',
71
71
  };
72
72
  }
@@ -79,6 +79,7 @@ export async function resolveProjectContext(cwd, options = {}) {
79
79
  endpoint: controlApiEndpoint,
80
80
  bearer: accountBearer,
81
81
  databaseId,
82
+ signInHint: host.signInHint,
82
83
  });
83
84
  if (probe.outcome === 'unreachable') {
84
85
  return {
@@ -140,7 +141,8 @@ export async function resolveProjectContext(cwd, options = {}) {
140
141
  reason: '',
141
142
  };
142
143
  }
143
- const NEEDS_AUTH_REASON = 'Not signed in to the app database yet. Sign in from the Sheets tab, or open the Preview and sign in to your app, then try again.';
144
+ const needsAuthReason = () => `Not signed in to the app database yet. ${getHost().signInHint ??
145
+ 'Sign in from the Sheets tab, or open the Preview and sign in to your app, then try again.'}`;
144
146
  // Resolve a data-plane token for record operations: serve the broker's active
145
147
  // account while valid, otherwise harvest the token the user created by signing
146
148
  // into their app in the Preview and adopt it into the broker. Also returns the
@@ -155,15 +157,15 @@ export async function resolveDataToken(context) {
155
157
  return { token: active.token, userId: active.userId };
156
158
  const preview = host.previewToken ? await host.previewToken() : null;
157
159
  if (!preview)
158
- return { reason: NEEDS_AUTH_REASON };
160
+ return { reason: needsAuthReason() };
159
161
  if (broker?.isInvalidToken(context.databaseId, preview.accessToken)) {
160
- return { reason: NEEDS_AUTH_REASON };
162
+ return { reason: needsAuthReason() };
161
163
  }
162
164
  const parsed = preview.accessTokenExpiresAt ? Date.parse(preview.accessTokenExpiresAt) : NaN;
163
165
  const expiresAt = Number.isNaN(parsed) ? Date.now() + 60 * 60 * 1000 : parsed;
164
166
  const skewMs = host.dataTokenSkewMs ?? DEFAULT_DATA_TOKEN_SKEW_MS;
165
167
  if (expiresAt <= Date.now() + skewMs)
166
- return { reason: NEEDS_AUTH_REASON };
168
+ return { reason: needsAuthReason() };
167
169
  broker?.adoptToken(context.databaseId, {
168
170
  userId: preview.userId,
169
171
  token: preview.accessToken,
package/esm/db-probe.d.ts CHANGED
@@ -26,4 +26,5 @@ export declare function probeDatabase(args: {
26
26
  bearer: string;
27
27
  databaseId: string;
28
28
  executor?: ProbeExecutor;
29
+ signInHint?: string;
29
30
  }): Promise<DatabaseProbe>;
package/esm/db-probe.js CHANGED
@@ -36,7 +36,7 @@ export async function probeDatabase(args) {
36
36
  if (AUTH_ERROR_RE.test(detail)) {
37
37
  return {
38
38
  outcome: 'unreachable',
39
- detail: `${detail} — the account credential was rejected; sign in again, then retry`,
39
+ detail: `${detail} — the account credential was rejected. ${args.signInHint ?? 'Sign in again, then retry.'}`,
40
40
  };
41
41
  }
42
42
  return { outcome: 'missing' };
package/esm/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/esm/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/esm/index.js CHANGED
@@ -4,7 +4,9 @@ import { configureHost } from './host';
4
4
  import { addPoliciesTool } from './tools/add-policies';
5
5
  import { addRecordsTool } from './tools/add-records';
6
6
  import { addRelationTool } from './tools/add-relation';
7
+ import { createApiKeyTool } from './tools/create-api-key';
7
8
  import { describeSchemaTool } from './tools/describe-schema';
9
+ import { manageEntityTypesTool } from './tools/manage-entity-types';
8
10
  import { createFieldTool, deleteFieldTool, deleteTableTool, updateFieldTool } from './tools/mutations';
9
11
  import { provisionBlueprintTool } from './tools/provision-blueprint';
10
12
  import { provisionDatabaseTool } from './tools/provision-database';
@@ -26,6 +28,8 @@ export const dbTools = (pi) => {
26
28
  pi.registerTool(updateTemplateTool);
27
29
  pi.registerTool(deleteTemplateTool);
28
30
  pi.registerTool(addRecordsTool);
31
+ pi.registerTool(manageEntityTypesTool);
32
+ pi.registerTool(createApiKeyTool);
29
33
  pi.registerTool(runCodegenTool);
30
34
  const gate = createConfirmGate({
31
35
  resolveProjectContext,
@@ -41,7 +45,7 @@ export function createDbTools(host) {
41
45
  return dbTools;
42
46
  }
43
47
  export { createConfirmGate } from './confirm-gate';
44
- export { resolveDataToken, resolveProjectContext, } from './context';
48
+ export { deriveSubdomainEndpoint, resolveDataToken, resolveProjectContext, } from './context';
45
49
  export { configureHost, getHost, } from './host';
46
50
  export { loadProvisionManifest, parseProvisionManifest, PROVISION_MANIFEST_FILE, } from './provision-database/manifest';
47
51
  export { allModulePresets, DEFAULT_PROVISION_PRESET, getModulePreset, } from './provision-database/presets';
@@ -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;
@@ -7,13 +7,13 @@
7
7
  // skip path). A signed-in session with no usable bearer (keychain locked) is an
8
8
  // explicit error, never a silent throwaway owner.
9
9
  // Side-effect-free so the branch is unit-testable without runtime/network.
10
- export function selectProvisionCredential(account) {
10
+ export function selectProvisionCredential(account, signInHint) {
11
11
  const bearer = account?.apiKey ?? account?.accessToken;
12
12
  if (account?.userId && bearer) {
13
13
  return { mode: 'account', ownerId: account.userId, bearer };
14
14
  }
15
15
  return {
16
16
  mode: 'error',
17
- reason: 'No usable account credential. Sign in (or unlock your session) before provisioning a database.',
17
+ reason: `No usable account credential. ${signInHint ?? 'Sign in (or unlock your session) before provisioning a database.'}`,
18
18
  };
19
19
  }
@@ -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,216 @@
1
+ import { auth } from '@constructive-io/sdk';
2
+ import { z } from 'zod';
3
+ import { deriveSubdomainEndpoint, resolveDataToken, resolveProjectContext } from '../context';
4
+ import { getHost } from '../host';
5
+ import { toolSchema } from '../tool-schema';
6
+ const CreateApiKeyZod = z.object({
7
+ key_name: z
8
+ .string()
9
+ .describe('Name for the API key (e.g. "deploy bot"). The .env variable name derives from it (UPPER_SNAKE + _API_KEY).'),
10
+ principal_name: z
11
+ .string()
12
+ .describe('Machine identity (principal) that owns the key. Defaults to key_name. An existing principal with this name is reused for unscoped keys.')
13
+ .optional(),
14
+ entity_ids: z
15
+ .array(z.string())
16
+ .describe('Entity row UUIDs to scope the principal to. Omit for a personal (unscoped) key that acts as the signed-in app user.')
17
+ .optional(),
18
+ read_only: z
19
+ .boolean()
20
+ .describe('Restrict the principal and key to read-only operations.')
21
+ .optional(),
22
+ expires_in_days: z
23
+ .number()
24
+ .int()
25
+ .positive()
26
+ .describe('Key lifetime in days. Default 90.')
27
+ .optional(),
28
+ });
29
+ const CreateApiKeySchema = toolSchema(CreateApiKeyZod);
30
+ function fail(message, extra) {
31
+ return {
32
+ content: [{ type: 'text', text: message }],
33
+ details: { success: false, message, ...extra },
34
+ };
35
+ }
36
+ export function toEnvVar(keyName) {
37
+ const base = keyName
38
+ .trim()
39
+ .toUpperCase()
40
+ .replace(/[^A-Z0-9]+/g, '_')
41
+ .replace(/^_+|_+$/g, '');
42
+ return base.endsWith('_API_KEY') ? base : `${base}_API_KEY`;
43
+ }
44
+ export function isStepUpError(messages) {
45
+ return messages.some((m) => /step[\s_-]?up/i.test(m));
46
+ }
47
+ export function describeScope(entityIds, readOnly) {
48
+ const parts = [];
49
+ parts.push(entityIds?.length
50
+ ? `scoped to ${entityIds.length} entit${entityIds.length === 1 ? 'y' : 'ies'}`
51
+ : 'unscoped (acts as the signed-in app user)');
52
+ if (readOnly)
53
+ parts.push('read-only');
54
+ return parts.join(', ');
55
+ }
56
+ async function rawGraphql(endpoint, token, query, variables) {
57
+ const res = await fetch(endpoint, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Content-Type': 'application/json',
61
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
62
+ },
63
+ body: JSON.stringify({ query, variables }),
64
+ });
65
+ return (await res.json());
66
+ }
67
+ async function createPrincipalInputFields(endpoint) {
68
+ const probe = await rawGraphql(endpoint, undefined, '{ __type(name: "CreatePrincipalInput") { inputFields { name } } }');
69
+ const type = probe.data?.__type;
70
+ return new Set((type?.inputFields ?? []).map((f) => f.name));
71
+ }
72
+ export const createApiKeyTool = {
73
+ name: 'create_api_key',
74
+ label: 'Create API key',
75
+ 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.',
76
+ 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.',
77
+ parameters: CreateApiKeySchema,
78
+ async execute(_id, params, _signal, _onUpdate, ctx) {
79
+ const keyName = params.key_name?.trim();
80
+ if (!keyName)
81
+ return fail('create_api_key requires "key_name".');
82
+ const resolved = await resolveProjectContext(ctx.cwd, { plane: 'data' });
83
+ if (!resolved.context)
84
+ return fail(resolved.reason);
85
+ const { databaseId, databaseName, apiEndpoint } = resolved.context;
86
+ if (!databaseName) {
87
+ return fail('Cannot resolve the app auth endpoint (DATABASE_NAME missing from .env). Re-provision the database, then retry.');
88
+ }
89
+ const host = getHost();
90
+ if (!host.deliverSecret) {
91
+ 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.');
92
+ }
93
+ const token = await resolveDataToken(resolved.context);
94
+ if (!token.token) {
95
+ return fail(token.reason ?? 'Sign in to the app database to create an API key.', {
96
+ needsAuth: true,
97
+ });
98
+ }
99
+ const authEndpoint = deriveSubdomainEndpoint(apiEndpoint, `auth-${databaseName}`);
100
+ if (!authEndpoint)
101
+ return fail('Cannot derive the app auth endpoint.');
102
+ const scoped = Boolean(params.entity_ids?.length) || params.read_only === true;
103
+ try {
104
+ if (scoped) {
105
+ const fields = await createPrincipalInputFields(authEndpoint);
106
+ const missing = [
107
+ ...(params.entity_ids?.length && !fields.has('entityIds') ? ['entityIds'] : []),
108
+ ...(params.read_only === true && !fields.has('isReadOnly') ? ['isReadOnly'] : []),
109
+ ];
110
+ if (missing.length) {
111
+ 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.`);
112
+ }
113
+ }
114
+ const dbAuth = auth.createClient({
115
+ endpoint: authEndpoint,
116
+ headers: { Authorization: `Bearer ${token.token}` },
117
+ });
118
+ const principalName = params.principal_name?.trim() || keyName;
119
+ const existing = await dbAuth.principal
120
+ .findFirst({
121
+ where: { name: { equalTo: principalName } },
122
+ select: { id: true, name: true, userId: true, isReadOnly: true },
123
+ })
124
+ .unwrap();
125
+ // createApiKey (like createPrincipal.result) takes the principal's
126
+ // identity userId; principal_entity.principalId references the row id.
127
+ let principalId = existing.principal?.userId;
128
+ if (existing.principal && scoped) {
129
+ 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.`);
130
+ }
131
+ if (existing.principal) {
132
+ const scopeRow = await dbAuth.principalEntity
133
+ .findFirst({
134
+ where: { principalId: { equalTo: existing.principal.id } },
135
+ select: { id: true },
136
+ })
137
+ .unwrap();
138
+ if (scopeRow.principalEntity || existing.principal?.isReadOnly) {
139
+ 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.`);
140
+ }
141
+ }
142
+ if (!principalId) {
143
+ // The SDK's PrincipalModel.create sends a nested {principal} input, but
144
+ // the live CreatePrincipalInput is flat and its payload only exposes
145
+ // `result: UUID` — raw GraphQL is the only working surface.
146
+ const created = await rawGraphql(authEndpoint, token.token, 'mutation ($input: CreatePrincipalInput!) { createPrincipal(input: $input) { result } }', {
147
+ input: {
148
+ name: principalName,
149
+ ...(params.entity_ids?.length && { entityIds: params.entity_ids }),
150
+ ...(params.read_only !== undefined && { isReadOnly: params.read_only }),
151
+ },
152
+ });
153
+ if (created.errors?.length) {
154
+ return fail(`createPrincipal failed: ${created.errors.map((e) => e.message).join('; ')}`);
155
+ }
156
+ const payload = created.data?.createPrincipal;
157
+ principalId = payload?.result;
158
+ if (!principalId)
159
+ return fail('createPrincipal returned no principal id.');
160
+ }
161
+ const mint = () => dbAuth.mutation
162
+ .createApiKey({
163
+ input: {
164
+ principalId: principalId,
165
+ keyName,
166
+ ...(params.read_only && { accessLevel: 'read_only' }),
167
+ expiresIn: { days: params.expires_in_days ?? 90 },
168
+ },
169
+ }, { select: { result: { select: { apiKey: true, keyId: true, expiresAt: true } } } })
170
+ .execute();
171
+ let minted = await mint();
172
+ if (!minted.ok && isStepUpError(minted.errors.map((e) => e.message))) {
173
+ if (!host.requestStepUp) {
174
+ 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.');
175
+ }
176
+ const verified = await host.requestStepUp({ databaseId, databaseName, apiEndpoint });
177
+ if (!verified)
178
+ return fail('Step-up verification was not completed. No key was minted.');
179
+ minted = await mint();
180
+ }
181
+ if (!minted.ok) {
182
+ return fail(`createApiKey failed: ${minted.errors.map((e) => e.message).join('; ')}`);
183
+ }
184
+ const record = minted.data?.createApiKey?.result;
185
+ if (!record?.apiKey || !record.keyId)
186
+ return fail('createApiKey returned no key.');
187
+ const envVar = toEnvVar(keyName);
188
+ await host.deliverSecret({
189
+ databaseId,
190
+ cwd: ctx.cwd,
191
+ envVar,
192
+ plaintext: record.apiKey,
193
+ keyId: record.keyId,
194
+ expiresAt: record.expiresAt ?? undefined,
195
+ });
196
+ const scope = describeScope(params.entity_ids, params.read_only === true);
197
+ 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}.` : ''}`;
198
+ return {
199
+ content: [{ type: 'text', text: message }],
200
+ details: {
201
+ success: true,
202
+ message,
203
+ keyId: record.keyId,
204
+ envVar,
205
+ expiresAt: record.expiresAt ?? undefined,
206
+ principalId,
207
+ principalName,
208
+ scope,
209
+ },
210
+ };
211
+ }
212
+ catch (err) {
213
+ return fail(err instanceof Error ? err.message : 'Failed to create the API key.');
214
+ }
215
+ },
216
+ };
@@ -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 {};