@agentic-kit/pi 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +5 -7
  2. package/confirm-gate.d.ts +1 -2
  3. package/esm/confirm-gate.d.ts +1 -2
  4. package/esm/host.d.ts +5 -110
  5. package/esm/host.js +5 -18
  6. package/esm/index.d.ts +17 -9
  7. package/esm/index.js +20 -38
  8. package/esm/pi-tool.d.ts +16 -0
  9. package/esm/pi-tool.js +32 -0
  10. package/host.d.ts +5 -110
  11. package/host.js +10 -22
  12. package/index.d.ts +17 -9
  13. package/index.js +47 -62
  14. package/package.json +6 -10
  15. package/pi-tool.d.ts +16 -0
  16. package/pi-tool.js +36 -0
  17. package/app-workspace.d.ts +0 -16
  18. package/app-workspace.js +0 -145
  19. package/context.d.ts +0 -62
  20. package/context.js +0 -282
  21. package/db-probe.d.ts +0 -30
  22. package/db-probe.js +0 -50
  23. package/esm/app-workspace.d.ts +0 -16
  24. package/esm/app-workspace.js +0 -134
  25. package/esm/context.d.ts +0 -62
  26. package/esm/context.js +0 -270
  27. package/esm/db-probe.d.ts +0 -30
  28. package/esm/db-probe.js +0 -47
  29. package/esm/policy/add-policies-to-table.d.ts +0 -26
  30. package/esm/policy/add-policies-to-table.js +0 -66
  31. package/esm/policy/provision-helpers.d.ts +0 -38
  32. package/esm/policy/provision-helpers.js +0 -50
  33. package/esm/provision-database/credential.d.ts +0 -14
  34. package/esm/provision-database/credential.js +0 -19
  35. package/esm/provision-database/env-file.d.ts +0 -37
  36. package/esm/provision-database/env-file.js +0 -85
  37. package/esm/provision-database/manifest.d.ts +0 -10
  38. package/esm/provision-database/manifest.js +0 -48
  39. package/esm/provision-database/pg-fixups.d.ts +0 -16
  40. package/esm/provision-database/pg-fixups.js +0 -132
  41. package/esm/provision-database/preset-match.d.ts +0 -13
  42. package/esm/provision-database/preset-match.js +0 -66
  43. package/esm/provision-database/presets.d.ts +0 -4
  44. package/esm/provision-database/presets.js +0 -13
  45. package/esm/provision-database/request-database.d.ts +0 -61
  46. package/esm/provision-database/request-database.js +0 -130
  47. package/esm/provision-database/resolve.d.ts +0 -27
  48. package/esm/provision-database/resolve.js +0 -49
  49. package/esm/records/meta.d.ts +0 -41
  50. package/esm/records/meta.js +0 -167
  51. package/esm/run-codegen/barrels.d.ts +0 -3
  52. package/esm/run-codegen/barrels.js +0 -48
  53. package/esm/run-codegen/endpoints.d.ts +0 -8
  54. package/esm/run-codegen/endpoints.js +0 -46
  55. package/esm/schema-resolve.d.ts +0 -23
  56. package/esm/schema-resolve.js +0 -69
  57. package/esm/tool-schema.d.ts +0 -3
  58. package/esm/tool-schema.js +0 -10
  59. package/esm/tools/add-policies.d.ts +0 -8
  60. package/esm/tools/add-policies.js +0 -96
  61. package/esm/tools/add-records.d.ts +0 -14
  62. package/esm/tools/add-records.js +0 -124
  63. package/esm/tools/add-relation-schema.d.ts +0 -30
  64. package/esm/tools/add-relation-schema.js +0 -58
  65. package/esm/tools/add-relation.d.ts +0 -10
  66. package/esm/tools/add-relation.js +0 -91
  67. package/esm/tools/create-api-key.d.ts +0 -18
  68. package/esm/tools/create-api-key.js +0 -216
  69. package/esm/tools/describe-schema.d.ts +0 -18
  70. package/esm/tools/describe-schema.js +0 -132
  71. package/esm/tools/manage-entity-types.d.ts +0 -41
  72. package/esm/tools/manage-entity-types.js +0 -203
  73. package/esm/tools/mutations.d.ts +0 -14
  74. package/esm/tools/mutations.js +0 -235
  75. package/esm/tools/provision-blueprint.d.ts +0 -22
  76. package/esm/tools/provision-blueprint.js +0 -111
  77. package/esm/tools/provision-database.d.ts +0 -14
  78. package/esm/tools/provision-database.js +0 -262
  79. package/esm/tools/run-codegen.d.ts +0 -11
  80. package/esm/tools/run-codegen.js +0 -139
  81. package/esm/tools/templates.d.ts +0 -49
  82. package/esm/tools/templates.js +0 -337
  83. package/policy/add-policies-to-table.d.ts +0 -26
  84. package/policy/add-policies-to-table.js +0 -71
  85. package/policy/provision-helpers.d.ts +0 -38
  86. package/policy/provision-helpers.js +0 -58
  87. package/provision-database/credential.d.ts +0 -14
  88. package/provision-database/credential.js +0 -22
  89. package/provision-database/env-file.d.ts +0 -37
  90. package/provision-database/env-file.js +0 -91
  91. package/provision-database/manifest.d.ts +0 -10
  92. package/provision-database/manifest.js +0 -56
  93. package/provision-database/pg-fixups.d.ts +0 -16
  94. package/provision-database/pg-fixups.js +0 -170
  95. package/provision-database/preset-match.d.ts +0 -13
  96. package/provision-database/preset-match.js +0 -70
  97. package/provision-database/presets.d.ts +0 -4
  98. package/provision-database/presets.js +0 -17
  99. package/provision-database/request-database.d.ts +0 -61
  100. package/provision-database/request-database.js +0 -134
  101. package/provision-database/resolve.d.ts +0 -27
  102. package/provision-database/resolve.js +0 -53
  103. package/records/meta.d.ts +0 -41
  104. package/records/meta.js +0 -172
  105. package/run-codegen/barrels.d.ts +0 -3
  106. package/run-codegen/barrels.js +0 -54
  107. package/run-codegen/endpoints.d.ts +0 -8
  108. package/run-codegen/endpoints.js +0 -51
  109. package/schema-resolve.d.ts +0 -23
  110. package/schema-resolve.js +0 -75
  111. package/tool-schema.d.ts +0 -3
  112. package/tool-schema.js +0 -13
  113. package/tools/add-policies.d.ts +0 -8
  114. package/tools/add-policies.js +0 -99
  115. package/tools/add-records.d.ts +0 -14
  116. package/tools/add-records.js +0 -127
  117. package/tools/add-relation-schema.d.ts +0 -30
  118. package/tools/add-relation-schema.js +0 -62
  119. package/tools/add-relation.d.ts +0 -10
  120. package/tools/add-relation.js +0 -94
  121. package/tools/create-api-key.d.ts +0 -18
  122. package/tools/create-api-key.js +0 -222
  123. package/tools/describe-schema.d.ts +0 -18
  124. package/tools/describe-schema.js +0 -135
  125. package/tools/manage-entity-types.d.ts +0 -41
  126. package/tools/manage-entity-types.js +0 -208
  127. package/tools/mutations.d.ts +0 -14
  128. package/tools/mutations.js +0 -238
  129. package/tools/provision-blueprint.d.ts +0 -22
  130. package/tools/provision-blueprint.js +0 -114
  131. package/tools/provision-database.d.ts +0 -14
  132. package/tools/provision-database.js +0 -268
  133. package/tools/run-codegen.d.ts +0 -11
  134. package/tools/run-codegen.js +0 -143
  135. package/tools/templates.d.ts +0 -49
  136. package/tools/templates.js +0 -341
@@ -1,127 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addRecordsTool = void 0;
4
- const generators_1 = require("@constructive-io/graphql-query/generators");
5
- const sdk_1 = require("@constructive-io/sdk");
6
- const zod_1 = require("zod");
7
- const context_1 = require("../context");
8
- const meta_1 = require("../records/meta");
9
- const tool_schema_1 = require("../tool-schema");
10
- function fail(message) {
11
- return { content: [{ type: 'text', text: message }], details: { success: false, message } };
12
- }
13
- function needsAuth(message) {
14
- return {
15
- content: [{ type: 'text', text: message }],
16
- details: { success: false, message, needsAuth: true },
17
- };
18
- }
19
- // Mirror @constructive-io/data's prepareCreateInput (create semantics): drop
20
- // undefined/null/empty-string so server defaults apply; keep false/0/[]/objects.
21
- function stripForCreate(record) {
22
- const out = {};
23
- for (const [key, value] of Object.entries(record)) {
24
- if (value === undefined || value === null || value === '')
25
- continue;
26
- out[key] = value;
27
- }
28
- return out;
29
- }
30
- const AddRecordsZod = zod_1.z.object({
31
- table_name: zod_1.z
32
- .string()
33
- .describe('Name of the table to insert rows into (snake_case, singular or plural).'),
34
- records: zod_1.z
35
- .array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()))
36
- .describe('Rows to insert. Each is an object of column → value (camelCase or snake_case column names).'),
37
- });
38
- const AddRecordsSchema = (0, tool_schema_1.toolSchema)(AddRecordsZod);
39
- exports.addRecordsTool = {
40
- name: 'add_records',
41
- label: 'Add records',
42
- description: "Insert one or more rows into a table in the project's app database.",
43
- promptSnippet: 'add_records: insert rows into an existing table. Gated.',
44
- parameters: AddRecordsSchema,
45
- async execute(_id, params, _signal, _onUpdate, ctx) {
46
- const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
47
- if (!resolved.context)
48
- return fail(resolved.reason);
49
- const { dataEndpoint } = resolved.context;
50
- if (!dataEndpoint) {
51
- return fail('Cannot resolve the app data endpoint (DATABASE_NAME missing from .env). Re-provision the database, then retry.');
52
- }
53
- if (params.records.length === 0)
54
- return fail('No records provided.');
55
- const token = await (0, context_1.resolveDataToken)(resolved.context);
56
- if (!token.token)
57
- return needsAuth(token.reason ?? 'Sign in to the database to insert records.');
58
- const adapter = new sdk_1.objects.FetchAdapter(dataEndpoint, {
59
- Authorization: `Bearer ${token.token}`,
60
- });
61
- let tables;
62
- try {
63
- const meta = await adapter.execute(meta_1.META_QUERY);
64
- if (!meta.ok) {
65
- return fail(`Failed to read schema from the app database: ${meta.errors.map((e) => e.message).join('; ')}`);
66
- }
67
- tables = (meta.data._meta?.tables ?? []).filter((t) => Boolean(t));
68
- }
69
- catch (err) {
70
- return fail(err instanceof Error ? err.message : 'Failed to query the app database schema.');
71
- }
72
- const rawTable = (0, meta_1.findMetaTable)(tables, params.table_name);
73
- if (!rawTable) {
74
- return fail(`Table "${params.table_name}" was not found in the app database.`);
75
- }
76
- const table = (0, meta_1.cleanTable)(rawTable);
77
- const allTables = tables.map(meta_1.cleanTable);
78
- const mutation = (0, generators_1.buildPostGraphileCreate)(table, allTables).toString();
79
- const mutationName = (0, generators_1.toCreateMutationName)(table.name, table);
80
- const singularName = (0, generators_1.toCamelCaseSingular)(table.name, table);
81
- // Constructive app tables scope rows by entityId (the owning user's id). The
82
- // create input requires it, but callers won't know it — backfill it with the
83
- // signed-in user's id from the Preview session when the table has the column
84
- // and the record omits it.
85
- const hasEntityId = table.fields.some((f) => f.name === 'entityId');
86
- const created = [];
87
- const errors = [];
88
- for (const record of params.records) {
89
- const prepared = hasEntityId && token.userId && record.entityId == null && record.entity_id == null
90
- ? { ...record, entityId: token.userId }
91
- : record;
92
- const variables = { input: { [singularName]: stripForCreate(prepared) } };
93
- try {
94
- const result = await adapter.execute(mutation, variables);
95
- if (!result.ok) {
96
- errors.push(result.errors.map((e) => e.message).join('; '));
97
- continue;
98
- }
99
- const payload = result.data[mutationName];
100
- const row = payload?.[singularName] ?? {};
101
- created.push(row);
102
- }
103
- catch (err) {
104
- errors.push(err instanceof Error ? err.message : 'Unknown error');
105
- }
106
- }
107
- if (created.length === 0) {
108
- return fail(`Failed to insert into "${params.table_name}": ${errors.join('; ') || 'unknown error'}`);
109
- }
110
- const columns = table.fields.map((f) => f.name);
111
- const message = errors.length > 0
112
- ? `Inserted ${created.length} of ${params.records.length} row(s) into ${params.table_name} (${errors.length} failed).`
113
- : `Inserted ${created.length} row(s) into ${params.table_name}.`;
114
- return {
115
- content: [{ type: 'text', text: message }],
116
- details: {
117
- success: true,
118
- message,
119
- tableName: rawTable.name,
120
- columns,
121
- rows: created,
122
- createdCount: created.length,
123
- asOf: new Date().toISOString(),
124
- },
125
- };
126
- },
127
- };
@@ -1,30 +0,0 @@
1
- import type { BlueprintDefinition } from '@agentic-kit/harness';
2
- import { z } from 'zod';
3
- export declare const DeleteAction: z.ZodEnum<{
4
- c: "c";
5
- r: "r";
6
- n: "n";
7
- a: "a";
8
- d: "d";
9
- }>;
10
- export declare const AddRelationZod: z.ZodObject<{
11
- relation_type: z.ZodOptional<z.ZodEnum<{
12
- belongs_to: "belongs_to";
13
- many_to_many: "many_to_many";
14
- }>>;
15
- source_table: z.ZodString;
16
- target_table: z.ZodString;
17
- field_name: z.ZodOptional<z.ZodString>;
18
- junction_table_name: z.ZodOptional<z.ZodString>;
19
- delete_action: z.ZodOptional<z.ZodEnum<{
20
- c: "c";
21
- r: "r";
22
- n: "n";
23
- a: "a";
24
- d: "d";
25
- }>>;
26
- is_required: z.ZodOptional<z.ZodBoolean>;
27
- }, z.core.$strip>;
28
- export declare const AddRelationSchema: import("typebox/type").TSchema;
29
- export type AddRelationParams = z.infer<typeof AddRelationZod>;
30
- export declare function buildRelation(params: AddRelationParams): BlueprintDefinition['relations'];
@@ -1,62 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AddRelationSchema = exports.AddRelationZod = exports.DeleteAction = void 0;
4
- exports.buildRelation = buildRelation;
5
- const zod_1 = require("zod");
6
- const tool_schema_1 = require("../tool-schema");
7
- // delete_action is intentionally always sent: the backend REJECTS a
8
- // RelationBelongsTo whose delete_action is absent ("PROVISION_RELATION:
9
- // delete_action is required"). We default it to 'a' (NO ACTION) so the model
10
- // never has to supply it.
11
- exports.DeleteAction = zod_1.z
12
- .enum(['c', 'r', 'n', 'a', 'd'])
13
- .describe('FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, a=NO ACTION (default), d=SET DEFAULT');
14
- exports.AddRelationZod = zod_1.z.object({
15
- relation_type: zod_1.z
16
- .enum(['belongs_to', 'many_to_many'])
17
- .describe('belongs_to adds an FK column on the source table; many_to_many adds a junction table. Defaults to belongs_to.')
18
- .optional(),
19
- source_table: zod_1.z.string().describe('Existing source table name (snake_case).'),
20
- target_table: zod_1.z.string().describe('Existing target table name (snake_case).'),
21
- field_name: zod_1.z
22
- .string()
23
- .describe('belongs_to only: FK column name to add on the source table, e.g. "author_id".')
24
- .optional(),
25
- junction_table_name: zod_1.z
26
- .string()
27
- .describe('many_to_many only: name of the junction table to create, e.g. "post_tags".')
28
- .optional(),
29
- delete_action: exports.DeleteAction.optional(),
30
- is_required: zod_1.z
31
- .boolean()
32
- .describe('belongs_to only: make the FK NOT NULL. Defaults to false — a NOT NULL FK fails if the source table already has rows.')
33
- .optional(),
34
- });
35
- exports.AddRelationSchema = (0, tool_schema_1.toolSchema)(exports.AddRelationZod);
36
- // Build a relation-only blueprint definition (no tables): the spike proved
37
- // constructBlueprint applies relations to PRE-EXISTING tables without recreating
38
- // them, as long as delete_action is supplied.
39
- function buildRelation(params) {
40
- const deleteAction = params.delete_action ?? 'a';
41
- if ((params.relation_type ?? 'belongs_to') === 'many_to_many') {
42
- return [
43
- {
44
- $type: 'RelationManyToMany',
45
- source_table: params.source_table,
46
- target_table: params.target_table,
47
- junction_table_name: params.junction_table_name,
48
- delete_action: deleteAction,
49
- },
50
- ];
51
- }
52
- return [
53
- {
54
- $type: 'RelationBelongsTo',
55
- source_table: params.source_table,
56
- target_table: params.target_table,
57
- field_name: params.field_name,
58
- delete_action: deleteAction,
59
- is_required: params.is_required ?? false,
60
- },
61
- ];
62
- }
@@ -1,10 +0,0 @@
1
- import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
2
- import { AddRelationSchema } from './add-relation-schema';
3
- export type AddRelationDetails = {
4
- success: boolean;
5
- message: string;
6
- sourceTable?: string;
7
- targetTable?: string;
8
- fieldName?: string;
9
- };
10
- export declare const addRelationTool: ToolDefinition<typeof AddRelationSchema, AddRelationDetails>;
@@ -1,94 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addRelationTool = void 0;
4
- const harness_1 = require("@agentic-kit/harness");
5
- const context_1 = require("../context");
6
- const schema_resolve_1 = require("../schema-resolve");
7
- const add_relation_schema_1 = require("./add-relation-schema");
8
- function fail(message) {
9
- return { content: [{ type: 'text', text: message }], details: { success: false, message } };
10
- }
11
- exports.addRelationTool = {
12
- name: 'add_relation',
13
- label: 'Add relation',
14
- description: 'Add a relation between two EXISTING tables without recreating them: belongs_to adds a foreign-key column on the source table; many_to_many creates a junction table. Use this for relations discovered after the tables were provisioned. For brand-new related tables, prefer a single provision_blueprint call.',
15
- promptSnippet: 'add_relation: link two existing tables (belongs_to FK column, or many_to_many junction). Gated.',
16
- parameters: add_relation_schema_1.AddRelationSchema,
17
- async execute(_id, params, _signal, _onUpdate, ctx) {
18
- const type = params.relation_type ?? 'belongs_to';
19
- if (type === 'belongs_to' && !params.field_name) {
20
- return fail('field_name is required for a belongs_to relation (the FK column name on the source table).');
21
- }
22
- if (type === 'many_to_many' && !params.junction_table_name) {
23
- return fail('junction_table_name is required for a many_to_many relation.');
24
- }
25
- const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
26
- if (!resolved.context)
27
- return fail(resolved.reason);
28
- const { modules, databaseId, schemaId, ownerId } = resolved.context;
29
- if (!ownerId)
30
- return fail('Not authenticated (missing OWNER_ID in .env).');
31
- // Validate both tables exist before constructing — a clearer error than the
32
- // backend's, and avoids a no-op blueprint.
33
- let schema;
34
- try {
35
- schema = await (0, schema_resolve_1.resolveSchema)(resolved.context);
36
- (0, schema_resolve_1.resolveTable)(schema, params.source_table);
37
- (0, schema_resolve_1.resolveTable)(schema, params.target_table);
38
- }
39
- catch (err) {
40
- return fail(err instanceof Error ? err.message : 'Failed to resolve schema.');
41
- }
42
- const definition = (0, harness_1.expandBlueprintDefaults)({ tables: [], relations: (0, add_relation_schema_1.buildRelation)(params) });
43
- const uniqueName = `add_relation_${Date.now()}`;
44
- try {
45
- const created = await modules.blueprint
46
- .create({
47
- data: {
48
- ownerId,
49
- databaseId,
50
- name: uniqueName,
51
- displayName: `${params.source_table} → ${params.target_table}`,
52
- description: `Add ${type} relation`,
53
- definition,
54
- },
55
- select: { id: true },
56
- })
57
- .unwrap();
58
- const blueprintId = created.createBlueprint?.blueprint?.id;
59
- if (!blueprintId)
60
- throw new Error('Failed to create relation blueprint');
61
- const constructResult = await modules.mutation
62
- .constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
63
- .unwrap();
64
- if (!constructResult.constructBlueprint?.result) {
65
- const constructions = await modules.blueprintConstruction
66
- .findMany({
67
- where: { blueprintId: { equalTo: blueprintId } },
68
- orderBy: ['ID_DESC'],
69
- first: 1,
70
- select: { status: true, errorDetails: true },
71
- })
72
- .unwrap();
73
- const error = constructions?.blueprintConstructions?.nodes?.[0]?.errorDetails ?? 'Unknown error';
74
- return fail(`Failed to add relation: ${typeof error === 'string' ? error : JSON.stringify(error)}`);
75
- }
76
- }
77
- catch (err) {
78
- return fail(`Failed to add relation: ${err instanceof Error ? err.message : String(err)}`);
79
- }
80
- const message = type === 'many_to_many'
81
- ? `Created junction table "${params.junction_table_name}" linking ${params.source_table} ↔ ${params.target_table}.`
82
- : `Added FK "${params.field_name}" on ${params.source_table} → ${params.target_table}.`;
83
- return {
84
- content: [{ type: 'text', text: message }],
85
- details: {
86
- success: true,
87
- message,
88
- sourceTable: params.source_table,
89
- targetTable: params.target_table,
90
- fieldName: params.field_name,
91
- },
92
- };
93
- },
94
- };
@@ -1,18 +0,0 @@
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 {};
@@ -1,222 +0,0 @@
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
- };
@@ -1,18 +0,0 @@
1
- import type { ToolDefinition } from '@earendil-works/pi-coding-agent';
2
- declare const ParamSchema: import("typebox/type").TSchema;
3
- type SchemaField = {
4
- name: string;
5
- type: string;
6
- isRequired: boolean;
7
- defaultValue: string | null;
8
- };
9
- type SchemaTable = {
10
- name: string;
11
- fields: SchemaField[];
12
- policies: string[];
13
- };
14
- export type DescribeSchemaDetails = {
15
- tables: SchemaTable[];
16
- };
17
- export declare const describeSchemaTool: ToolDefinition<typeof ParamSchema, DescribeSchemaDetails>;
18
- export {};