@hexis-ai/engram-server 0.13.0 → 0.15.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.
@@ -6,11 +6,20 @@ export declare class InMemoryKeyStore implements KeyStore {
6
6
  private readonly workspaces;
7
7
  private readonly keys;
8
8
  private readonly byHash;
9
+ /** Mirror of engram_workspaces.org_id. Lookup-only — fakes / org-stores
10
+ * in tests read this to answer membership queries without re-tracking
11
+ * the binding themselves. */
12
+ private readonly orgIds;
9
13
  createWorkspace(input: {
10
14
  id?: string;
11
15
  name?: string;
12
16
  metadata?: Record<string, unknown>;
17
+ orgId?: string;
13
18
  }): Promise<Workspace>;
19
+ /** Org id stamped on a workspace at creation, or undefined if not org-scoped. */
20
+ getWorkspaceOrgId(workspaceId: string): string | undefined;
21
+ /** All (workspaceId, orgId) pairs. Read-only view for fakes. */
22
+ workspaceOrgBindings(): ReadonlyMap<string, string>;
14
23
  getWorkspace(id: string): Promise<Workspace | null>;
15
24
  listWorkspaces(): Promise<Workspace[]>;
16
25
  updateWorkspace(id: string, patch: {
@@ -6,6 +6,10 @@ export class InMemoryKeyStore {
6
6
  workspaces = new Map();
7
7
  keys = new Map();
8
8
  byHash = new Map();
9
+ /** Mirror of engram_workspaces.org_id. Lookup-only — fakes / org-stores
10
+ * in tests read this to answer membership queries without re-tracking
11
+ * the binding themselves. */
12
+ orgIds = new Map();
9
13
  async createWorkspace(input) {
10
14
  const id = resolveWorkspaceId(input);
11
15
  const existing = this.workspaces.get(id);
@@ -18,8 +22,18 @@ export class InMemoryKeyStore {
18
22
  createdAt: new Date().toISOString(),
19
23
  };
20
24
  this.workspaces.set(id, ws);
25
+ if (input.orgId !== undefined)
26
+ this.orgIds.set(id, input.orgId);
21
27
  return ws;
22
28
  }
29
+ /** Org id stamped on a workspace at creation, or undefined if not org-scoped. */
30
+ getWorkspaceOrgId(workspaceId) {
31
+ return this.orgIds.get(workspaceId);
32
+ }
33
+ /** All (workspaceId, orgId) pairs. Read-only view for fakes. */
34
+ workspaceOrgBindings() {
35
+ return this.orgIds;
36
+ }
23
37
  async getWorkspace(id) {
24
38
  return this.workspaces.get(id) ?? null;
25
39
  }
@@ -40,6 +54,7 @@ export class InMemoryKeyStore {
40
54
  }
41
55
  async deleteWorkspace(id) {
42
56
  this.workspaces.delete(id);
57
+ this.orgIds.delete(id);
43
58
  for (const [keyId, row] of this.keys) {
44
59
  if (row.workspaceId === id) {
45
60
  this.byHash.delete(row.keyHash);
@@ -42,6 +42,12 @@ export class InMemoryAdapter {
42
42
  ...(init.trigger_event_id != null
43
43
  ? { trigger_event_id: init.trigger_event_id }
44
44
  : {}),
45
+ ...(init.trigger_purpose != null
46
+ ? { trigger_purpose: init.trigger_purpose }
47
+ : {}),
48
+ ...(init.trigger_resume_hint != null
49
+ ? { trigger_resume_hint: init.trigger_resume_hint }
50
+ : {}),
45
51
  },
46
52
  events: new Map(),
47
53
  });
@@ -12,6 +12,7 @@ export declare class PostgresKeyStore implements KeyStore {
12
12
  id?: string;
13
13
  name?: string;
14
14
  metadata?: Record<string, unknown>;
15
+ orgId?: string;
15
16
  }): Promise<Workspace>;
16
17
  getWorkspace(id: string): Promise<Workspace | null>;
17
18
  listWorkspaces(): Promise<Workspace[]>;
@@ -16,8 +16,13 @@ export class PostgresKeyStore {
16
16
  async createWorkspace(input) {
17
17
  const id = resolveWorkspaceId(input);
18
18
  await this.sql `
19
- INSERT INTO engram_workspaces (id, name, metadata)
20
- VALUES (${id}, ${input.name ?? null}, ${(input.metadata ?? {})})
19
+ INSERT INTO engram_workspaces (id, name, metadata, org_id)
20
+ VALUES (
21
+ ${id},
22
+ ${input.name ?? null},
23
+ ${(input.metadata ?? {})},
24
+ ${input.orgId ?? null}
25
+ )
21
26
  ON CONFLICT (id) DO NOTHING
22
27
  `;
23
28
  const ws = await this.getWorkspace(id);
@@ -27,7 +27,6 @@ export declare class PostgresOrgStore implements OrgStore {
27
27
  email: string;
28
28
  } | null>;
29
29
  listOrgsForUser(userId: string): Promise<OrgMembershipRow[]>;
30
- setWorkspaceOrg(workspaceId: string, orgId: string): Promise<void>;
31
30
  listWorkspacesForOrg(orgId: string): Promise<{
32
31
  id: string;
33
32
  name: string | null;
@@ -89,9 +89,8 @@ export class PostgresOrgStore {
89
89
  return rows.map(toMember);
90
90
  }
91
91
  // ---------- workspace ↔ org link -----------------------------
92
- async setWorkspaceOrg(workspaceId, orgId) {
93
- await this.pool.query(`UPDATE engram_workspaces SET org_id = $1 WHERE id = $2`, [orgId, workspaceId]);
94
- }
92
+ // engram_workspaces.org_id is written by KeyStore.createWorkspace in
93
+ // the same INSERT as the workspace row. This store only reads.
95
94
  async listWorkspacesForOrg(orgId) {
96
95
  const { rows } = await this.pool.query(`SELECT id, name FROM engram_workspaces WHERE org_id = $1 ORDER BY created_at`, [orgId]);
97
96
  return rows;
@@ -31,7 +31,8 @@ export class PostgresAdapter {
31
31
  INSERT INTO engram_sessions (
32
32
  workspace_id, id, title, channel, participants, viewable_by,
33
33
  created_at, updated_at, status, summary, model,
34
- trigger_conversation_id, trigger_event_id
34
+ trigger_conversation_id, trigger_event_id,
35
+ trigger_purpose, trigger_resume_hint
35
36
  )
36
37
  VALUES (
37
38
  ${this.workspaceId},
@@ -46,7 +47,9 @@ export class PostgresAdapter {
46
47
  ${init.summary ?? null},
47
48
  ${init.model ?? null},
48
49
  ${init.trigger_conversation_id ?? null},
49
- ${init.trigger_event_id ?? null}
50
+ ${init.trigger_event_id ?? null},
51
+ ${init.trigger_purpose ?? null},
52
+ ${init.trigger_resume_hint ?? null}
50
53
  )
51
54
  ON CONFLICT (workspace_id, id) DO NOTHING
52
55
  `;
@@ -97,7 +100,8 @@ export class PostgresAdapter {
97
100
  const rows = await this.sql `
98
101
  SELECT id, title, channel, participants, viewable_by, created_at,
99
102
  updated_at, status, summary, model,
100
- trigger_conversation_id, trigger_event_id
103
+ trigger_conversation_id, trigger_event_id,
104
+ trigger_purpose, trigger_resume_hint
101
105
  FROM engram_sessions
102
106
  WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
103
107
  LIMIT 1
@@ -122,6 +126,8 @@ export class PostgresAdapter {
122
126
  const model = pickPatch(patch, "model");
123
127
  const tcId = pickPatch(patch, "trigger_conversation_id");
124
128
  const teId = pickPatch(patch, "trigger_event_id");
129
+ const tPurpose = pickPatch(patch, "trigger_purpose");
130
+ const tResume = pickPatch(patch, "trigger_resume_hint");
125
131
  const rows = await this.sql `
126
132
  UPDATE engram_sessions SET
127
133
  title = CASE WHEN ${title.provided} THEN ${title.value} ELSE title END,
@@ -133,6 +139,10 @@ export class PostgresAdapter {
133
139
  THEN ${tcId.value} ELSE trigger_conversation_id END,
134
140
  trigger_event_id = CASE WHEN ${teId.provided}
135
141
  THEN ${teId.value} ELSE trigger_event_id END,
142
+ trigger_purpose = CASE WHEN ${tPurpose.provided}
143
+ THEN ${tPurpose.value} ELSE trigger_purpose END,
144
+ trigger_resume_hint = CASE WHEN ${tResume.provided}
145
+ THEN ${tResume.value} ELSE trigger_resume_hint END,
136
146
  updated_at = now()
137
147
  WHERE workspace_id = ${this.workspaceId} AND id = ${sessionId}
138
148
  RETURNING id
@@ -474,6 +484,12 @@ function toSessionRow(r) {
474
484
  ...(r.trigger_event_id !== null
475
485
  ? { trigger_event_id: r.trigger_event_id }
476
486
  : {}),
487
+ ...(r.trigger_purpose !== null
488
+ ? { trigger_purpose: r.trigger_purpose }
489
+ : {}),
490
+ ...(r.trigger_resume_hint !== null
491
+ ? { trigger_resume_hint: r.trigger_resume_hint }
492
+ : {}),
477
493
  };
478
494
  }
479
495
  function toAliasInfo(r) {
package/dist/admin.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { Hono } from "hono";
2
- import { type KeyStore } from "./key-store";
2
+ import type { KeyStore } from "./key-store";
3
3
  import type { OrgStore } from "./org-store";
4
4
  export interface AdminOptions {
5
5
  /** Bearer token required for every /admin/v1 request. */
6
6
  token: string;
7
7
  keyStore: KeyStore;
8
8
  /**
9
- * Optional org store. When provided, mounts the orgs surface
10
- * (\`/admin/v1/orgs/*\` plus \`/admin/v1/orgs/:id/workspaces\` for
11
- * org-scoped workspace creation). Without it only the legacy
12
- * \`/admin/v1/workspaces\` endpoints are exposed.
9
+ * Org store. Required every admin endpoint is org-scoped.
10
+ * (The previous opt-out, where admin.ts also exposed a non-org
11
+ * `/admin/v1/workspaces` surface for `orgStore`-less deployments,
12
+ * was removed in v0.15.)
13
13
  */
14
- orgStore?: OrgStore;
14
+ orgStore: OrgStore;
15
15
  }
16
16
  interface Env {
17
17
  Variables: {
@@ -24,20 +24,20 @@ interface Env {
24
24
  * Auth model: a single platform-level bearer token
25
25
  * (\`ENGRAM_ADMIN_TOKEN\`). Never crosses with workspace api-keys.
26
26
  *
27
- * Surface, when orgStore is wired:
27
+ * Surface:
28
28
  *
29
- * POST /orgs create org
30
- * GET /orgs list orgs
31
- * GET /orgs/:id get org
32
- * DELETE /orgs/:id delete org (CASCADE)
33
- * POST /orgs/:id/members add member (email|userId)
29
+ * POST /orgs create org
30
+ * GET /orgs list orgs
31
+ * GET /orgs/:id get org
32
+ * DELETE /orgs/:id delete org (CASCADE)
33
+ * POST /orgs/:id/members add member (email|userId)
34
34
  * GET /orgs/:id/members
35
35
  * DELETE /orgs/:id/members/:userId
36
- * POST /orgs/:id/workspaces create + key (under org)
36
+ * POST /orgs/:id/workspaces create workspace + key
37
37
  * GET /orgs/:id/workspaces
38
- *
39
- * Plus the lower-level workspace + key endpoints from before
40
- * (\`/workspaces\`, \`/workspaces/:id/keys\`, ...).
38
+ * GET /orgs/:id/workspaces/:wsId/keys list workspace keys
39
+ * POST /orgs/:id/workspaces/:wsId/keys issue a new key
40
+ * DELETE /orgs/:id/workspaces/:wsId/keys/:keyId revoke a key
41
41
  */
42
42
  export declare function createAdminRouter(opts: AdminOptions): Hono<Env>;
43
43
  export {};
package/dist/admin.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Hono } from "hono";
2
- import { isValidWorkspaceId } from "./key-store";
3
2
  import { createWorkspaceSchema, issueKeySchema, parseJsonBody } from "./schemas";
4
3
  import { addMember, createOrg, createWorkspaceUnderOrg, deleteOrg, getOrgOrThrow, OrgServiceError, removeMember, requireWorkspaceInOrg, revokeWorkspaceKey, } from "./services/orgs";
5
4
  /**
@@ -8,23 +7,25 @@ import { addMember, createOrg, createWorkspaceUnderOrg, deleteOrg, getOrgOrThrow
8
7
  * Auth model: a single platform-level bearer token
9
8
  * (\`ENGRAM_ADMIN_TOKEN\`). Never crosses with workspace api-keys.
10
9
  *
11
- * Surface, when orgStore is wired:
10
+ * Surface:
12
11
  *
13
- * POST /orgs create org
14
- * GET /orgs list orgs
15
- * GET /orgs/:id get org
16
- * DELETE /orgs/:id delete org (CASCADE)
17
- * POST /orgs/:id/members add member (email|userId)
12
+ * POST /orgs create org
13
+ * GET /orgs list orgs
14
+ * GET /orgs/:id get org
15
+ * DELETE /orgs/:id delete org (CASCADE)
16
+ * POST /orgs/:id/members add member (email|userId)
18
17
  * GET /orgs/:id/members
19
18
  * DELETE /orgs/:id/members/:userId
20
- * POST /orgs/:id/workspaces create + key (under org)
19
+ * POST /orgs/:id/workspaces create workspace + key
21
20
  * GET /orgs/:id/workspaces
22
- *
23
- * Plus the lower-level workspace + key endpoints from before
24
- * (\`/workspaces\`, \`/workspaces/:id/keys\`, ...).
21
+ * GET /orgs/:id/workspaces/:wsId/keys list workspace keys
22
+ * POST /orgs/:id/workspaces/:wsId/keys issue a new key
23
+ * DELETE /orgs/:id/workspaces/:wsId/keys/:keyId revoke a key
25
24
  */
26
25
  export function createAdminRouter(opts) {
27
26
  const app = new Hono();
27
+ const { orgStore } = opts;
28
+ const deps = { orgStore, keyStore: opts.keyStore };
28
29
  app.use("*", async (c, next) => {
29
30
  const supplied = c.req.header("x-admin-token") ??
30
31
  c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
@@ -33,94 +34,6 @@ export function createAdminRouter(opts) {
33
34
  }
34
35
  await next();
35
36
  });
36
- // -------------------- workspaces (legacy / api-key-only) ----
37
- // Kept for backwards compatibility; new callers should create
38
- // workspaces under an org so they're reachable from the web UI.
39
- app.post("/workspaces", async (c) => {
40
- const body = await parseJsonBody(c, createWorkspaceSchema);
41
- if (body instanceof Response)
42
- return body;
43
- if (body.id !== undefined && !isValidWorkspaceId(body.id)) {
44
- return c.json({ error: "invalid_workspace_id" }, 400);
45
- }
46
- try {
47
- const ws = await opts.keyStore.createWorkspace({
48
- ...(body.id !== undefined ? { id: body.id } : {}),
49
- ...(body.name !== undefined ? { name: body.name } : {}),
50
- ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
51
- });
52
- if (body.issueKey === false) {
53
- return c.json({ workspace: ws });
54
- }
55
- const key = await opts.keyStore.issueKey(ws.id, {
56
- ...(body.keyName !== undefined ? { name: body.keyName } : {}),
57
- });
58
- return c.json({ workspace: ws, key });
59
- }
60
- catch (e) {
61
- return c.json({ error: e.message }, 400);
62
- }
63
- });
64
- app.get("/workspaces", async (c) => {
65
- const workspaces = await opts.keyStore.listWorkspaces();
66
- return c.json({ workspaces });
67
- });
68
- app.get("/workspaces/:id", async (c) => {
69
- const ws = await opts.keyStore.getWorkspace(c.req.param("id"));
70
- if (!ws)
71
- return c.json({ error: "workspace_not_found" }, 404);
72
- return c.json(ws);
73
- });
74
- app.delete("/workspaces/:id", async (c) => {
75
- const id = c.req.param("id");
76
- const ws = await opts.keyStore.getWorkspace(id);
77
- if (!ws)
78
- return c.json({ error: "workspace_not_found" }, 404);
79
- await opts.keyStore.deleteWorkspace(id);
80
- return c.body(null, 204);
81
- });
82
- app.post("/workspaces/:id/keys", async (c) => {
83
- const workspaceId = c.req.param("id");
84
- const ws = await opts.keyStore.getWorkspace(workspaceId);
85
- if (!ws)
86
- return c.json({ error: "workspace_not_found" }, 404);
87
- const raw = await c.req.json().catch(() => ({}));
88
- const parsed = issueKeySchema.safeParse(raw);
89
- if (!parsed.success) {
90
- return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
91
- }
92
- const key = await opts.keyStore.issueKey(workspaceId, {
93
- ...(parsed.data.name !== undefined ? { name: parsed.data.name } : {}),
94
- });
95
- return c.json(key);
96
- });
97
- app.get("/workspaces/:id/keys", async (c) => {
98
- const workspaceId = c.req.param("id");
99
- const ws = await opts.keyStore.getWorkspace(workspaceId);
100
- if (!ws)
101
- return c.json({ error: "workspace_not_found" }, 404);
102
- const keys = await opts.keyStore.listKeys(workspaceId);
103
- return c.json({ keys });
104
- });
105
- app.delete("/workspaces/:id/keys/:keyId", async (c) => {
106
- const workspaceId = c.req.param("id");
107
- const keyId = c.req.param("keyId");
108
- try {
109
- await opts.keyStore.revokeKey(workspaceId, keyId);
110
- }
111
- catch (e) {
112
- if (e.message === "key_not_found") {
113
- return c.json({ error: "key_not_found" }, 404);
114
- }
115
- throw e;
116
- }
117
- return c.body(null, 204);
118
- });
119
- // -------------------- orgs (org-scoped admin surface) -------
120
- const orgStore = opts.orgStore;
121
- if (!orgStore)
122
- return app;
123
- const deps = { orgStore, keyStore: opts.keyStore };
124
37
  app.post("/orgs", async (c) => runService(c, async () => {
125
38
  const body = (await c.req.json().catch(() => ({})));
126
39
  const org = await createOrg(deps, body);
@@ -158,9 +71,9 @@ export function createAdminRouter(opts) {
158
71
  return c.body(null, 204);
159
72
  }));
160
73
  // ----- org workspaces ---------------------------------------
161
- // Stands up a tenant in one round-trip. createWorkspaceUnderOrg
162
- // wraps createWorkspace + setWorkspaceOrg + issueKey and applies
163
- // the same id validation as the legacy /workspaces POST.
74
+ // Stands up a tenant. createWorkspaceUnderOrg writes the workspace
75
+ // + org binding in a single INSERT (atomic) and then issues the
76
+ // initial API key (separate insert; retry-safe via ON CONFLICT).
164
77
  app.post("/orgs/:id/workspaces", async (c) => runService(c, async () => {
165
78
  const orgId = c.req.param("id");
166
79
  await getOrgOrThrow(deps, orgId);
@@ -29,10 +29,18 @@ export interface KeyResolution {
29
29
  keyId: string;
30
30
  }
31
31
  export interface KeyStore {
32
+ /**
33
+ * Persist a workspace row. `orgId`, when supplied, lands in the same
34
+ * INSERT so the workspace and its org binding are committed atomically
35
+ * (no orphaned workspace if a follow-up `setWorkspaceOrg` would have
36
+ * failed). Legacy callers without an org still work — `org_id` stays
37
+ * NULL.
38
+ */
32
39
  createWorkspace(input: {
33
40
  id?: string;
34
41
  name?: string;
35
42
  metadata?: Record<string, unknown>;
43
+ orgId?: string;
36
44
  }): Promise<Workspace>;
37
45
  getWorkspace(id: string): Promise<Workspace | null>;
38
46
  listWorkspaces(): Promise<Workspace[]>;
package/dist/main.js CHANGED
@@ -22,6 +22,10 @@ const CORS_ORIGINS = (process.env.ENGRAM_CORS_ORIGINS ?? "")
22
22
  .split(",")
23
23
  .map((s) => s.trim())
24
24
  .filter(Boolean);
25
+ if (!orgStore) {
26
+ console.error("[engram-server] orgStore is required (DATABASE_URL + ENGRAM_AUTH_* env vars) — every admin endpoint is now org-scoped");
27
+ process.exit(1);
28
+ }
25
29
  const app = createServer({
26
30
  auth: async (key) => {
27
31
  const r = await keyStore.resolveKey(key);
@@ -31,14 +35,10 @@ const app = createServer({
31
35
  },
32
36
  ...(authHandler ? { authHandler } : {}),
33
37
  ...(cookieAuth ? { cookieAuth } : {}),
34
- ...(orgStore ? { orgStore } : {}),
38
+ orgStore,
35
39
  keyStore,
36
40
  ...(CORS_ORIGINS.length > 0 ? { corsOrigins: CORS_ORIGINS } : {}),
37
- admin: {
38
- token: ADMIN_TOKEN,
39
- keyStore,
40
- ...(orgStore ? { orgStore } : {}),
41
- },
41
+ admin: { token: ADMIN_TOKEN, keyStore, orgStore },
42
42
  });
43
43
  console.log(`[engram-server] listening on :${PORT}`);
44
44
  export default { port: PORT, fetch: app.fetch };
@@ -0,0 +1,2 @@
1
+ export declare const name = "0008-trigger-metadata";
2
+ export declare const sql = "\n-- Hosts that open a side-conversation with a stated goal (monet's\n-- start_conversation tool) need to thread two free-text fields through\n-- engram for the agent's resume flow:\n--\n-- trigger_purpose what this side-conv exists to accomplish\n-- trigger_resume_hint the condition that should resume the parent\n--\n-- Both are opaque to engram \u2014 we store and serve them verbatim. Nullable\n-- because most sessions aren't side-conversations.\n\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_purpose TEXT;\nALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_resume_hint TEXT;\n";
@@ -0,0 +1,15 @@
1
+ export const name = "0008-trigger-metadata";
2
+ export const sql = `
3
+ -- Hosts that open a side-conversation with a stated goal (monet's
4
+ -- start_conversation tool) need to thread two free-text fields through
5
+ -- engram for the agent's resume flow:
6
+ --
7
+ -- trigger_purpose what this side-conv exists to accomplish
8
+ -- trigger_resume_hint the condition that should resume the parent
9
+ --
10
+ -- Both are opaque to engram — we store and serve them verbatim. Nullable
11
+ -- because most sessions aren't side-conversations.
12
+
13
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_purpose TEXT;
14
+ ALTER TABLE engram_sessions ADD COLUMN IF NOT EXISTS trigger_resume_hint TEXT;
15
+ `;
@@ -5,6 +5,7 @@ import * as m0004 from "./0004-schema-completion";
5
5
  import * as m0005 from "./0005-session-updated-at";
6
6
  import * as m0006 from "./0006-auth";
7
7
  import * as m0007 from "./0007-orgs";
8
+ import * as m0008 from "./0008-trigger-metadata";
8
9
  /**
9
10
  * Schema migrations, applied in array order. Add a new file under
10
11
  * `migrations/NNNN-<slug>.ts` exporting `name` and `sql`, then append it
@@ -20,4 +21,5 @@ export const MIGRATIONS = [
20
21
  { name: m0005.name, sql: m0005.sql },
21
22
  { name: m0006.name, sql: m0006.sql },
22
23
  { name: m0007.name, sql: m0007.sql },
24
+ { name: m0008.name, sql: m0008.sql },
23
25
  ];
package/dist/openapi.js CHANGED
@@ -71,14 +71,6 @@ const TAG_DEFS = [
71
71
  name: "Orgs (admin)",
72
72
  description: "プラットフォーム管理者トークンでの org 管理",
73
73
  },
74
- {
75
- name: "Workspaces (admin)",
76
- description: "ワークスペースの管理(管理者トークン必須)",
77
- },
78
- {
79
- name: "API Keys (admin)",
80
- description: "ワークスペース API キーの発行・無効化(管理者トークン必須)",
81
- },
82
74
  ];
83
75
  /**
84
76
  * Stamp `tag` onto every operation in a path-item. The path declares its
@@ -634,87 +626,6 @@ function buildPaths() {
634
626
  },
635
627
  },
636
628
  }),
637
- "/admin/v1/workspaces": tagged("Workspaces (admin)", {
638
- post: {
639
- summary: "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
640
- security: adminAuth,
641
- requestBody: jsonBody("CreateWorkspace"),
642
- responses: {
643
- "200": res("ワークスペース(issueKey=false でない限りキーも含む)"),
644
- "400": res("リクエストボディまたはワークスペース id が不正"),
645
- "401": res("認証エラー"),
646
- },
647
- },
648
- get: {
649
- summary: "全ワークスペースを一覧取得する。",
650
- security: adminAuth,
651
- responses: {
652
- "200": res("ワークスペース一覧"),
653
- "401": res("認証エラー"),
654
- },
655
- },
656
- }),
657
- "/admin/v1/workspaces/{id}": tagged("Workspaces (admin)", {
658
- get: {
659
- summary: "単一のワークスペースを取得する。",
660
- security: adminAuth,
661
- parameters: [pathParam("id", "ワークスペース id。")],
662
- responses: {
663
- "200": res("ワークスペース"),
664
- "404": res("ワークスペースが見つからない"),
665
- "401": res("認証エラー"),
666
- },
667
- },
668
- delete: {
669
- summary: "ワークスペースを削除する(キー・セッション・イベントにカスケードする)。",
670
- security: adminAuth,
671
- parameters: [pathParam("id", "ワークスペース id。")],
672
- responses: {
673
- "204": res("削除完了"),
674
- "404": res("ワークスペースが見つからない"),
675
- "401": res("認証エラー"),
676
- },
677
- },
678
- }),
679
- "/admin/v1/workspaces/{id}/keys": tagged("API Keys (admin)", {
680
- post: {
681
- summary: "ワークスペースに新しい API キーを発行する。",
682
- security: adminAuth,
683
- parameters: [pathParam("id", "ワークスペース id。")],
684
- requestBody: jsonBody("IssueKey", false),
685
- responses: {
686
- "200": res("発行されたキー(生のキーは一度のみ返却)"),
687
- "400": res("リクエストボディが不正"),
688
- "404": res("ワークスペースが見つからない"),
689
- "401": res("認証エラー"),
690
- },
691
- },
692
- get: {
693
- summary: "ワークスペースの API キー一覧を取得する(ハッシュのみ)。",
694
- security: adminAuth,
695
- parameters: [pathParam("id", "ワークスペース id。")],
696
- responses: {
697
- "200": res("キー一覧"),
698
- "404": res("ワークスペースが見つからない"),
699
- "401": res("認証エラー"),
700
- },
701
- },
702
- }),
703
- "/admin/v1/workspaces/{id}/keys/{keyId}": tagged("API Keys (admin)", {
704
- delete: {
705
- summary: "API キーを無効化する。",
706
- security: adminAuth,
707
- parameters: [
708
- pathParam("id", "ワークスペース id。"),
709
- pathParam("keyId", "キー id。"),
710
- ],
711
- responses: {
712
- "204": res("無効化完了(冪等)"),
713
- "404": res("キーが見つからない"),
714
- "401": res("認証エラー"),
715
- },
716
- },
717
- }),
718
629
  };
719
630
  }
720
631
  /**
@@ -48,12 +48,6 @@ export interface OrgStore {
48
48
  } | null>;
49
49
  /** Orgs the given user is a member of. */
50
50
  listOrgsForUser(userId: string): Promise<OrgMembershipRow[]>;
51
- /**
52
- * Set the org an already-existing workspace belongs to. Called
53
- * by the admin endpoint that creates a workspace under an org
54
- * (key issuance and engram_workspaces.org_id are written together).
55
- */
56
- setWorkspaceOrg(workspaceId: string, orgId: string): Promise<void>;
57
51
  /** Workspaces in the given org. */
58
52
  listWorkspacesForOrg(orgId: string): Promise<{
59
53
  id: string;
package/dist/schemas.d.ts CHANGED
@@ -24,6 +24,8 @@ export declare const sessionInitSchema: z.ZodObject<{
24
24
  model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
25
  trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
26
  trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ trigger_purpose: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
+ trigger_resume_hint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
29
  }, z.core.$strip>;
28
30
  export declare const sessionUpdateSchema: z.ZodObject<{
29
31
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -37,6 +39,8 @@ export declare const sessionUpdateSchema: z.ZodObject<{
37
39
  model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
40
  trigger_conversation_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
41
  trigger_event_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ trigger_purpose: z.ZodOptional<z.ZodNullable<z.ZodString>>;
43
+ trigger_resume_hint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
44
  }, z.core.$strip>;
41
45
  export declare const sessionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
42
46
  type: z.ZodLiteral<"step">;
package/dist/schemas.js CHANGED
@@ -20,6 +20,8 @@ export const sessionInitSchema = z.object({
20
20
  model: z.string().nullable().optional(),
21
21
  trigger_conversation_id: z.string().nullable().optional(),
22
22
  trigger_event_id: z.string().nullable().optional(),
23
+ trigger_purpose: z.string().nullable().optional(),
24
+ trigger_resume_hint: z.string().nullable().optional(),
23
25
  });
24
26
  export const sessionUpdateSchema = z.object({
25
27
  title: z.string().nullable().optional(),
@@ -29,6 +31,8 @@ export const sessionUpdateSchema = z.object({
29
31
  model: z.string().nullable().optional(),
30
32
  trigger_conversation_id: z.string().nullable().optional(),
31
33
  trigger_event_id: z.string().nullable().optional(),
34
+ trigger_purpose: z.string().nullable().optional(),
35
+ trigger_resume_hint: z.string().nullable().optional(),
32
36
  });
33
37
  const stepEventSchema = z.object({
34
38
  type: z.literal("step"),
@@ -3,8 +3,9 @@
3
3
  * (`/admin/v1/*`) and the cookie-auth self-service surface (`/v1/orgs/*`).
4
4
  *
5
5
  * Auth and HTTP framing stay in the route modules. This file owns:
6
- * - the multi-step orchestrations (createWorkspace + setWorkspaceOrg
7
- * + issueKey; email→userId lookup + upsertMember)
6
+ * - the multi-step orchestrations (createWorkspaceUnderOrg writes
7
+ * the workspace + org binding atomically and then issues a key;
8
+ * email→userId lookup + upsertMember)
8
9
  * - the safety invariants (workspace id validation, last-owner
9
10
  * protection on member removal)
10
11
  * - the canonical error → status mapping (OrgServiceError.status)
@@ -70,12 +71,15 @@ export interface CreateWorkspaceResult {
70
71
  key?: IssuedKey;
71
72
  }
72
73
  /**
73
- * Stand up a new workspace under an org in one round-trip:
74
- * createWorkspace setWorkspaceOrg (optional) issueKey.
74
+ * Stand up a new workspace under an org. The workspace + org binding
75
+ * commit atomically because the KeyStore writes `org_id` in the same
76
+ * INSERT as the workspace row. Issuing the initial API key remains a
77
+ * follow-up call (idempotent — a retry of the workspace POST returns
78
+ * the existing row via ON CONFLICT DO NOTHING).
75
79
  *
76
80
  * Validates the workspace id shape up front so a bad id fails before
77
- * touching either store. Returns the workspace with `orgId` stamped on
78
- * so callers don't need a second lookup.
81
+ * touching the store. Returns the workspace with `orgId` stamped on so
82
+ * callers don't need a second lookup.
79
83
  */
80
84
  export declare function createWorkspaceUnderOrg(deps: OrgServiceDeps, orgId: string, body: CreateWorkspaceInput): Promise<CreateWorkspaceResult>;
81
85
  export declare function updateOrgWorkspace(deps: OrgServiceDeps, orgId: string, wsId: string, body: {
@@ -3,8 +3,9 @@
3
3
  * (`/admin/v1/*`) and the cookie-auth self-service surface (`/v1/orgs/*`).
4
4
  *
5
5
  * Auth and HTTP framing stay in the route modules. This file owns:
6
- * - the multi-step orchestrations (createWorkspace + setWorkspaceOrg
7
- * + issueKey; email→userId lookup + upsertMember)
6
+ * - the multi-step orchestrations (createWorkspaceUnderOrg writes
7
+ * the workspace + org binding atomically and then issues a key;
8
+ * email→userId lookup + upsertMember)
8
9
  * - the safety invariants (workspace id validation, last-owner
9
10
  * protection on member removal)
10
11
  * - the canonical error → status mapping (OrgServiceError.status)
@@ -92,12 +93,15 @@ export async function removeMember(deps, orgId, userId) {
92
93
  await deps.orgStore.removeMember(orgId, userId);
93
94
  }
94
95
  /**
95
- * Stand up a new workspace under an org in one round-trip:
96
- * createWorkspace setWorkspaceOrg (optional) issueKey.
96
+ * Stand up a new workspace under an org. The workspace + org binding
97
+ * commit atomically because the KeyStore writes `org_id` in the same
98
+ * INSERT as the workspace row. Issuing the initial API key remains a
99
+ * follow-up call (idempotent — a retry of the workspace POST returns
100
+ * the existing row via ON CONFLICT DO NOTHING).
97
101
  *
98
102
  * Validates the workspace id shape up front so a bad id fails before
99
- * touching either store. Returns the workspace with `orgId` stamped on
100
- * so callers don't need a second lookup.
103
+ * touching the store. Returns the workspace with `orgId` stamped on so
104
+ * callers don't need a second lookup.
101
105
  */
102
106
  export async function createWorkspaceUnderOrg(deps, orgId, body) {
103
107
  if (body.id !== undefined && !isValidWorkspaceId(body.id)) {
@@ -108,8 +112,8 @@ export async function createWorkspaceUnderOrg(deps, orgId, body) {
108
112
  ...(body.id !== undefined ? { id: body.id } : {}),
109
113
  ...(body.name !== undefined ? { name: body.name } : {}),
110
114
  ...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
115
+ orgId,
111
116
  });
112
- await deps.orgStore.setWorkspaceOrg(ws.id, orgId);
113
117
  if (body.issueKey === false) {
114
118
  return { workspace: { ...ws, orgId } };
115
119
  }
package/dist/storage.d.ts CHANGED
@@ -147,6 +147,8 @@ export interface SessionRow {
147
147
  model?: string;
148
148
  trigger_conversation_id?: string;
149
149
  trigger_event_id?: string;
150
+ trigger_purpose?: string;
151
+ trigger_resume_hint?: string;
150
152
  }
151
153
  export declare function foldEvents(row: SessionRow, events: SessionEvent[], now: Date): Session;
152
154
  /**
package/dist/storage.js CHANGED
@@ -35,6 +35,12 @@ export function foldEvents(row, events, now) {
35
35
  ...(row.trigger_event_id !== undefined
36
36
  ? { trigger_event_id: row.trigger_event_id }
37
37
  : {}),
38
+ ...(row.trigger_purpose !== undefined
39
+ ? { trigger_purpose: row.trigger_purpose }
40
+ : {}),
41
+ ...(row.trigger_resume_hint !== undefined
42
+ ? { trigger_resume_hint: row.trigger_resume_hint }
43
+ : {}),
38
44
  updated_at: row.updatedAt,
39
45
  };
40
46
  }
package/openapi.json CHANGED
@@ -37,14 +37,6 @@
37
37
  {
38
38
  "name": "Orgs (admin)",
39
39
  "description": "プラットフォーム管理者トークンでの org 管理"
40
- },
41
- {
42
- "name": "Workspaces (admin)",
43
- "description": "ワークスペースの管理(管理者トークン必須)"
44
- },
45
- {
46
- "name": "API Keys (admin)",
47
- "description": "ワークスペース API キーの発行・無効化(管理者トークン必須)"
48
40
  }
49
41
  ],
50
42
  "servers": [
@@ -145,6 +137,26 @@
145
137
  "type": "null"
146
138
  }
147
139
  ]
140
+ },
141
+ "trigger_purpose": {
142
+ "anyOf": [
143
+ {
144
+ "type": "string"
145
+ },
146
+ {
147
+ "type": "null"
148
+ }
149
+ ]
150
+ },
151
+ "trigger_resume_hint": {
152
+ "anyOf": [
153
+ {
154
+ "type": "string"
155
+ },
156
+ {
157
+ "type": "null"
158
+ }
159
+ ]
148
160
  }
149
161
  },
150
162
  "additionalProperties": false
@@ -219,6 +231,26 @@
219
231
  "type": "null"
220
232
  }
221
233
  ]
234
+ },
235
+ "trigger_purpose": {
236
+ "anyOf": [
237
+ {
238
+ "type": "string"
239
+ },
240
+ {
241
+ "type": "null"
242
+ }
243
+ ]
244
+ },
245
+ "trigger_resume_hint": {
246
+ "anyOf": [
247
+ {
248
+ "type": "string"
249
+ },
250
+ {
251
+ "type": "null"
252
+ }
253
+ ]
222
254
  }
223
255
  },
224
256
  "additionalProperties": false
@@ -2462,252 +2494,6 @@
2462
2494
  "Orgs (admin)"
2463
2495
  ]
2464
2496
  }
2465
- },
2466
- "/admin/v1/workspaces": {
2467
- "post": {
2468
- "summary": "ワークスペースを作成する(デフォルトで初期キーも発行する)。",
2469
- "security": [
2470
- {
2471
- "adminToken": []
2472
- }
2473
- ],
2474
- "requestBody": {
2475
- "required": true,
2476
- "content": {
2477
- "application/json": {
2478
- "schema": {
2479
- "$ref": "#/components/schemas/CreateWorkspace"
2480
- }
2481
- }
2482
- }
2483
- },
2484
- "responses": {
2485
- "200": {
2486
- "description": "ワークスペース(issueKey=false でない限りキーも含む)"
2487
- },
2488
- "400": {
2489
- "description": "リクエストボディまたはワークスペース id が不正"
2490
- },
2491
- "401": {
2492
- "description": "認証エラー"
2493
- }
2494
- },
2495
- "tags": [
2496
- "Workspaces (admin)"
2497
- ]
2498
- },
2499
- "get": {
2500
- "summary": "全ワークスペースを一覧取得する。",
2501
- "security": [
2502
- {
2503
- "adminToken": []
2504
- }
2505
- ],
2506
- "responses": {
2507
- "200": {
2508
- "description": "ワークスペース一覧"
2509
- },
2510
- "401": {
2511
- "description": "認証エラー"
2512
- }
2513
- },
2514
- "tags": [
2515
- "Workspaces (admin)"
2516
- ]
2517
- }
2518
- },
2519
- "/admin/v1/workspaces/{id}": {
2520
- "get": {
2521
- "summary": "単一のワークスペースを取得する。",
2522
- "security": [
2523
- {
2524
- "adminToken": []
2525
- }
2526
- ],
2527
- "parameters": [
2528
- {
2529
- "name": "id",
2530
- "in": "path",
2531
- "required": true,
2532
- "schema": {
2533
- "type": "string"
2534
- },
2535
- "description": "ワークスペース id。"
2536
- }
2537
- ],
2538
- "responses": {
2539
- "200": {
2540
- "description": "ワークスペース"
2541
- },
2542
- "401": {
2543
- "description": "認証エラー"
2544
- },
2545
- "404": {
2546
- "description": "ワークスペースが見つからない"
2547
- }
2548
- },
2549
- "tags": [
2550
- "Workspaces (admin)"
2551
- ]
2552
- },
2553
- "delete": {
2554
- "summary": "ワークスペースを削除する(キー・セッション・イベントにカスケードする)。",
2555
- "security": [
2556
- {
2557
- "adminToken": []
2558
- }
2559
- ],
2560
- "parameters": [
2561
- {
2562
- "name": "id",
2563
- "in": "path",
2564
- "required": true,
2565
- "schema": {
2566
- "type": "string"
2567
- },
2568
- "description": "ワークスペース id。"
2569
- }
2570
- ],
2571
- "responses": {
2572
- "204": {
2573
- "description": "削除完了"
2574
- },
2575
- "401": {
2576
- "description": "認証エラー"
2577
- },
2578
- "404": {
2579
- "description": "ワークスペースが見つからない"
2580
- }
2581
- },
2582
- "tags": [
2583
- "Workspaces (admin)"
2584
- ]
2585
- }
2586
- },
2587
- "/admin/v1/workspaces/{id}/keys": {
2588
- "post": {
2589
- "summary": "ワークスペースに新しい API キーを発行する。",
2590
- "security": [
2591
- {
2592
- "adminToken": []
2593
- }
2594
- ],
2595
- "parameters": [
2596
- {
2597
- "name": "id",
2598
- "in": "path",
2599
- "required": true,
2600
- "schema": {
2601
- "type": "string"
2602
- },
2603
- "description": "ワークスペース id。"
2604
- }
2605
- ],
2606
- "requestBody": {
2607
- "required": false,
2608
- "content": {
2609
- "application/json": {
2610
- "schema": {
2611
- "$ref": "#/components/schemas/IssueKey"
2612
- }
2613
- }
2614
- }
2615
- },
2616
- "responses": {
2617
- "200": {
2618
- "description": "発行されたキー(生のキーは一度のみ返却)"
2619
- },
2620
- "400": {
2621
- "description": "リクエストボディが不正"
2622
- },
2623
- "401": {
2624
- "description": "認証エラー"
2625
- },
2626
- "404": {
2627
- "description": "ワークスペースが見つからない"
2628
- }
2629
- },
2630
- "tags": [
2631
- "API Keys (admin)"
2632
- ]
2633
- },
2634
- "get": {
2635
- "summary": "ワークスペースの API キー一覧を取得する(ハッシュのみ)。",
2636
- "security": [
2637
- {
2638
- "adminToken": []
2639
- }
2640
- ],
2641
- "parameters": [
2642
- {
2643
- "name": "id",
2644
- "in": "path",
2645
- "required": true,
2646
- "schema": {
2647
- "type": "string"
2648
- },
2649
- "description": "ワークスペース id。"
2650
- }
2651
- ],
2652
- "responses": {
2653
- "200": {
2654
- "description": "キー一覧"
2655
- },
2656
- "401": {
2657
- "description": "認証エラー"
2658
- },
2659
- "404": {
2660
- "description": "ワークスペースが見つからない"
2661
- }
2662
- },
2663
- "tags": [
2664
- "API Keys (admin)"
2665
- ]
2666
- }
2667
- },
2668
- "/admin/v1/workspaces/{id}/keys/{keyId}": {
2669
- "delete": {
2670
- "summary": "API キーを無効化する。",
2671
- "security": [
2672
- {
2673
- "adminToken": []
2674
- }
2675
- ],
2676
- "parameters": [
2677
- {
2678
- "name": "id",
2679
- "in": "path",
2680
- "required": true,
2681
- "schema": {
2682
- "type": "string"
2683
- },
2684
- "description": "ワークスペース id。"
2685
- },
2686
- {
2687
- "name": "keyId",
2688
- "in": "path",
2689
- "required": true,
2690
- "schema": {
2691
- "type": "string"
2692
- },
2693
- "description": "キー id。"
2694
- }
2695
- ],
2696
- "responses": {
2697
- "204": {
2698
- "description": "無効化完了(冪等)"
2699
- },
2700
- "401": {
2701
- "description": "認証エラー"
2702
- },
2703
- "404": {
2704
- "description": "キーが見つからない"
2705
- }
2706
- },
2707
- "tags": [
2708
- "API Keys (admin)"
2709
- ]
2710
- }
2711
2497
  }
2712
2498
  }
2713
2499
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexis-ai/engram-server",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
5
5
  "keywords": [
6
6
  "engram",
@@ -49,8 +49,8 @@
49
49
  "dev": "bun --hot src/dev.ts"
50
50
  },
51
51
  "dependencies": {
52
- "@hexis-ai/engram-core": "^0.2.0",
53
- "@hexis-ai/engram-sdk": "^0.14.0",
52
+ "@hexis-ai/engram-core": "^0.3.0",
53
+ "@hexis-ai/engram-sdk": "^0.16.0",
54
54
  "better-auth": "^1.6.11",
55
55
  "hono": "^4.6.0",
56
56
  "pg": "^8.13.0",