@agentic-kit/pi 0.9.2 → 0.10.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/context.js +7 -5
- package/db-probe.d.ts +1 -0
- package/db-probe.js +1 -1
- package/esm/context.js +7 -5
- package/esm/db-probe.d.ts +1 -0
- package/esm/db-probe.js +1 -1
- package/esm/host.d.ts +40 -0
- package/esm/index.d.ts +1 -1
- package/esm/index.js +5 -1
- package/esm/provision-database/credential.d.ts +1 -1
- package/esm/provision-database/credential.js +2 -2
- package/esm/tools/create-api-key.d.ts +18 -0
- package/esm/tools/create-api-key.js +216 -0
- package/esm/tools/manage-entity-types.d.ts +41 -0
- package/esm/tools/manage-entity-types.js +203 -0
- package/esm/tools/provision-database.js +2 -1
- package/host.d.ts +40 -0
- package/index.d.ts +1 -1
- package/index.js +6 -1
- package/package.json +5 -5
- package/provision-database/credential.d.ts +1 -1
- package/provision-database/credential.js +2 -2
- package/tools/create-api-key.d.ts +18 -0
- package/tools/create-api-key.js +222 -0
- package/tools/manage-entity-types.d.ts +41 -0
- package/tools/manage-entity-types.js +208 -0
- package/tools/provision-database.js +2 -1
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:
|
|
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
|
|
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:
|
|
169
|
+
return { reason: needsAuthReason() };
|
|
168
170
|
if (broker?.isInvalidToken(context.databaseId, preview.accessToken)) {
|
|
169
|
-
return { 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:
|
|
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
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
|
|
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:
|
|
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
|
|
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:
|
|
160
|
+
return { reason: needsAuthReason() };
|
|
159
161
|
if (broker?.isInvalidToken(context.databaseId, preview.accessToken)) {
|
|
160
|
-
return { 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:
|
|
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
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
|
|
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:
|
|
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 {};
|