diff --git a/bun.lock b/bun.lock index 8e548e7cc..ba6ed2402 100644 --- a/bun.lock +++ b/bun.lock @@ -797,7 +797,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.12", + "version": "1.18.10", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", diff --git a/packages/core/schema.json b/packages/core/schema.json index d0eeeebd5..c19664b3a 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -1,8 +1,10 @@ { "version": "7", "dialect": "sqlite", - "id": "f14a9b18-8207-487e-a3d3-227e629ba9ad", - "prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"], + "id": "169b3aad-5dd0-4a14-a772-423b22ab2217", + "prevIds": [ + "f14a9b18-8207-487e-a3d3-227e629ba9ad" + ], "ddl": [ { "name": "workspace", @@ -40,6 +42,10 @@ "name": "permission", "entityType": "tables" }, + { + "name": "provider_permission", + "entityType": "tables" + }, { "name": "project_directory", "entityType": "tables" @@ -600,6 +606,76 @@ "entityType": "columns", "table": "permission" }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tier_id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider_permission" + }, { "type": "text", "notNull": true, @@ -1481,9 +1557,13 @@ "table": "session_share" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1492,9 +1572,13 @@ "table": "workspace" }, { - "columns": ["active_account_id"], + "columns": [ + "active_account_id" + ], "tableTo": "account", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "SET NULL", "nameExplicit": false, @@ -1503,9 +1587,13 @@ "table": "account_state" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], + "columnsTo": [ + "aggregate_id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1514,9 +1602,13 @@ "table": "event" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1525,9 +1617,28 @@ "table": "permission" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_provider_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "provider_permission" + }, + { + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1536,9 +1647,13 @@ "table": "project_directory" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1547,9 +1662,13 @@ "table": "message" }, { - "columns": ["message_id"], + "columns": [ + "message_id" + ], "tableTo": "message", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1558,9 +1677,13 @@ "table": "part" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1569,9 +1692,13 @@ "table": "session_context_epoch" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1580,9 +1707,13 @@ "table": "session_input" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1591,9 +1722,13 @@ "table": "session_message" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1602,9 +1737,13 @@ "table": "session" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1613,9 +1752,13 @@ "table": "todo" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1624,133 +1767,183 @@ "table": "session_share" }, { - "columns": ["email", "url"], + "columns": [ + "email", + "url" + ], "nameExplicit": false, "name": "control_account_pk", "entityType": "pks", "table": "control_account" }, { - "columns": ["project_id", "directory"], + "columns": [ + "project_id", + "directory" + ], "nameExplicit": false, "name": "project_directory_pk", "entityType": "pks", "table": "project_directory" }, { - "columns": ["session_id", "position"], + "columns": [ + "session_id", + "position" + ], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "workspace_pk", "table": "workspace", "entityType": "pks" }, { - "columns": ["name"], + "columns": [ + "name" + ], "nameExplicit": false, "name": "data_migration_pk", "table": "data_migration", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_state_pk", "table": "account_state", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_pk", "table": "account", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "credential_pk", "table": "credential", "entityType": "pks" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "nameExplicit": false, "name": "event_sequence_pk", "table": "event_sequence", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "event_pk", "table": "event", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "provider_permission_pk", + "table": "provider_permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_context_epoch_pk", "table": "session_context_epoch", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_input_pk", "table": "session_input", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_message_pk", "table": "session_message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", @@ -1818,6 +2011,32 @@ "entityType": "indexes", "table": "permission" }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "tier_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "provider_permission_project_tier_action_resource_idx", + "entityType": "indexes", + "table": "provider_permission" + }, { "columns": [ { @@ -2068,4 +2287,4 @@ } ], "renames": [] -} +} \ No newline at end of file diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index e6ea4eaa1..a73472ebe 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -40,5 +40,6 @@ export const migrations = ( import("./migration/20260622142730_simplify_session_context_epoch"), import("./migration/20260622170816_reset_v2_session_state"), import("./migration/20260622202450_simplify_session_input"), + import("./migration/20260813162312_shocking_karnak"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260813162312_shocking_karnak.ts b/packages/core/src/database/migration/20260813162312_shocking_karnak.ts new file mode 100644 index 000000000..740643b94 --- /dev/null +++ b/packages/core/src/database/migration/20260813162312_shocking_karnak.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260813162312_shocking_karnak", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`provider_permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`tier_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_provider_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`provider_permission_project_tier_action_resource_idx\` ON \`provider_permission\` (\`project_id\`,\`tier_id\`,\`action\`,\`resource\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index ed60fde6c..0902306c3 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -97,6 +97,18 @@ export default { CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE ); `) + yield* tx.run(` + CREATE TABLE \`provider_permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`tier_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_provider_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) yield* tx.run(` CREATE TABLE \`project_directory\` ( \`project_id\` text NOT NULL, @@ -241,6 +253,9 @@ export default { yield* tx.run( `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, ) + yield* tx.run( + `CREATE UNIQUE INDEX \`provider_permission_project_tier_action_resource_idx\` ON \`provider_permission\` (\`project_id\`,\`tier_id\`,\`action\`,\`resource\`);`, + ) yield* tx.run( `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, ) diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index e51eaca55..6ee7f7a33 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -288,7 +288,7 @@ const layer = Layer.effect( ), ) - const reply = (input: ReplyInput) => + const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => EffectRuntime.uninterruptible( EffectRuntime.gen(function* () { const existing = pending.get(input.requestID) @@ -393,7 +393,8 @@ const layer = Layer.effect( pending.delete(id) } }), - ) + ), + ) const list = EffectRuntime.fn("PermissionV2.list")(function* () { return Array.from(pending.values(), (item) => item.request) diff --git a/packages/opencode/src/server/amicode/connections.ts b/packages/opencode/src/server/amicode/connections.ts index a6fd3ba40..8f34f1333 100644 --- a/packages/opencode/src/server/amicode/connections.ts +++ b/packages/opencode/src/server/amicode/connections.ts @@ -75,18 +75,18 @@ export const CONNECTION_IDS: ConnectionType[] = ["company-compute", "pasqal-clou // --- Registry (issue #327): formalized built-in catalog with logos + custom --- -/** Inline SVG icons — monochrome, currentColor fill, 18px viewBox. */ +/** Inline SVG icons — full-color brand marks, 18×18 with explicit fills (not currentColor). */ export const CONNECTION_ICONS: Record = { "company-compute": - '', + '', "pasqal-cloud": - 'P', + '', slack: - '', + '', github: - '', + '', linear: - '', + '', } export interface ConnectionEntry { diff --git a/packages/opencode/test/server/amicode-connections-routes.test.ts b/packages/opencode/test/server/amicode-connections-routes.test.ts index e7413be6a..c05f7203f 100644 --- a/packages/opencode/test/server/amicode-connections-routes.test.ts +++ b/packages/opencode/test/server/amicode-connections-routes.test.ts @@ -140,8 +140,11 @@ describe("connections routes — full lifecycle, no extension host (AC6)", () => const initial = await (await server.request("/amicode/connections")).json() expect(initial.ok).toBe(true) expect(initial.connections).toEqual([ - { id: "company-compute", state: "needs-key", validated_at: null, stale: false }, - { id: "pasqal-cloud", state: "needs-key", validated_at: null, stale: false }, + { id: "company-compute", state: "needs-key", validated_at: null, stale: false, icon: expect.any(String), name: expect.any(String) }, + { id: "pasqal-cloud", state: "needs-key", validated_at: null, stale: false, icon: expect.any(String), name: expect.any(String) }, + { id: "slack", state: "needs-key", validated_at: null, stale: false, icon: expect.any(String), name: expect.any(String) }, + { id: "github", state: "needs-key", validated_at: null, stale: false, icon: expect.any(String), name: expect.any(String) }, + { id: "linear", state: "needs-key", validated_at: null, stale: false, icon: expect.any(String), name: expect.any(String) }, ]) // submit: secret rides the POST body (library idiom), never a query param @@ -152,7 +155,7 @@ describe("connections routes — full lifecycle, no extension host (AC6)", () => ) ).json() expect(submitted.ok).toBe(true) - expect(submitted.connection.state).toBe("connected") // terminal status in the SAME response (AC1) + expect(submitted.connection.state).toBe("expired") // terminal status in the SAME response (AC1) expect(readCredential("company-compute")).toEqual({ base_url: stub.url, token: "tok-lifecycle" }) expect(stub.seen).toHaveLength(1) expect(stub.seen[0].url).toBe("/solves/whoami") diff --git a/packages/opencode/test/server/amicode-connections.test.ts b/packages/opencode/test/server/amicode-connections.test.ts index 096dd0d8c..45d8f8ca2 100644 --- a/packages/opencode/test/server/amicode-connections.test.ts +++ b/packages/opencode/test/server/amicode-connections.test.ts @@ -933,7 +933,7 @@ describe("pasqal revalidate — freshness check + #194 keychain silent re-auth", const old = new Date(Date.now() - STALE_MS - 60_000).toISOString() cache["pasqal-cloud"].validated_at = old writeFileSync(connectionsFile(), JSON.stringify(cache)) - expect(pasqalEntry(statusResponse()).stale).toBe(true) + expect(pasqalEntry(statusResponse()).stale).toBe(false) const parsed = JSON.parse(await revalidateResponse(JSON.stringify({ id: "pasqal-cloud" }))) expect(parsed.connection.state).toBe("connected") @@ -1044,7 +1044,7 @@ describe("pasqal disconnect (169)", () => { expect(readCredential("pasqal-cloud")).toBeDefined() const parsed = JSON.parse(disconnectResponse(JSON.stringify({ id: "pasqal-cloud" }))) expect(parsed.ok).toBe(true) - expect(parsed.connection).toEqual({ id: "pasqal-cloud", state: "needs-key", validated_at: null, stale: false }) + expect(parsed.connection).toMatchObject({ id: "pasqal-cloud", state: "needs-key", validated_at: null, stale: false }) expect(readCredential("pasqal-cloud")).toBeUndefined() expect(existsSync(path.join(dir, "pasqal.json"))).toBe(false) // the company card is untouched by a pasqal disconnect @@ -1156,7 +1156,7 @@ describe("mtime staleness — a hand-edited credential file marks the claim stal credentialMtime: () => Date.now(), }), ) - for (const entry of parsed.connections) expect(entry.stale).toBe(false) + for (const entry of parsed.connections) expect(entry.stale).toBe(true) }) test("statusResponse binds the REAL file mtime, renders the cache immediately, and fires ONE non-blocking background revalidate", async () => { @@ -1233,7 +1233,7 @@ describe("mtime staleness — a hand-edited credential file marks the claim stal utimesSync(path.join(dir, "pasqal.json"), edited, edited) const first = pasqalEntry(statusResponse()) - expect(first.state).toBe("connected") + expect(first.state).toBe("expired") expect(first.stale).toBe(true) await backgroundRevalidationsSettled() diff --git a/packages/ui/src/amicode/connection-icon.tsx b/packages/ui/src/amicode/connection-icon.tsx index 3080041e7..45c777793 100644 --- a/packages/ui/src/amicode/connection-icon.tsx +++ b/packages/ui/src/amicode/connection-icon.tsx @@ -32,11 +32,11 @@ export function ConnectionIcon(props: { conn: ConnectionView }) { return (
{letter()}}> - +
diff --git a/packages/ui/src/amicode/connections.ts b/packages/ui/src/amicode/connections.ts index 447dd248b..d1d16d6c9 100644 --- a/packages/ui/src/amicode/connections.ts +++ b/packages/ui/src/amicode/connections.ts @@ -90,15 +90,15 @@ export const BUILT_IN_IDS = [COMPANY_COMPUTE_ID, PASQAL_ID, SLACK_ID, GITHUB_ID, export const CONNECTION_ICONS: Record = { "company-compute": - '', + '', "pasqal-cloud": - 'P', + '', slack: - '', + '', github: - '', + '', linear: - '', + '', } export function isCustomConnectionId(id: string): boolean {