@pylonsync/sdk 0.4.22 → 0.4.24

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/dist/index.d.ts CHANGED
@@ -434,6 +434,12 @@ export interface ActionDefinition {
434
434
  input?: InputFieldDefinition[];
435
435
  /** See `QueryDefinition.auth`. */
436
436
  auth?: string;
437
+ /** Set by `discoverFunctions` for `agent()` definitions —
438
+ * `buildManifest` injects the AgentRun/AgentMessage entities when
439
+ * any action carries it, so templates that pass `actions:
440
+ * fns.actions` (rather than spreading the whole result) still get
441
+ * the injection. Not serialized into the manifest. */
442
+ isAgent?: boolean;
437
443
  /**
438
444
  * Which write kind this was before the manifest's read/write split
439
445
  * collapsed mutations and actions into one bucket. Preserved so a
@@ -780,6 +786,9 @@ export declare function discoverAppRoutes(opts?: {
780
786
  export interface DiscoveredFunctions {
781
787
  queries: QueryDefinition[];
782
788
  actions: ActionDefinition[];
789
+ /** True when any discovered function is an `agent()` — buildManifest
790
+ * then injects the AgentRun/AgentMessage entities + policies. */
791
+ hasAgents?: boolean;
783
792
  }
784
793
  /**
785
794
  * Walk `functions/` and return every externally callable function, for
@@ -1059,6 +1068,11 @@ export declare function buildManifest(options: {
1059
1068
  connections?: ManifestConnection[];
1060
1069
  crons?: ManifestCron[];
1061
1070
  fonts?: ManifestFont[];
1071
+ /** Set by `discoverFunctions()` (spread its result into this call).
1072
+ * When true, the framework's AgentRun/AgentMessage entities and
1073
+ * their owner-scoping policies are appended to the manifest —
1074
+ * unless the app already declares entities with those names. */
1075
+ hasAgents?: boolean;
1062
1076
  }): AppManifest;
1063
1077
  export { defineStudioConfig, defineStudioExtensions, type BrandConfig, type ThemeConfig, type ThemeAccent, type ThemeAppearance, type IconName, type SidebarConfig, type SidebarSection, type SidebarItem, type SidebarPageItem, type SidebarResourceItem, type SidebarLinkItem, type SidebarHeadingItem, type SidebarFooter, type SidebarFooterCard, type SidebarFooterCustom, type ResourceConfig, type ResourceListConfig, type ColumnConfig, type ColumnRenderer, type RendererKind, type RendererText, type RendererAvatar, type RendererBadge, type RendererDate, type RendererLink, type RendererBoolean, type RendererNumber, type RendererJson, type RendererCustom, type BulkAction, type RowAction, type PageConfig, type StudioConfig, type StudioCellRendererProps, type StudioPageProps, type StudioExtensions, } from "./studio";
1064
1078
  /**
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.22",
6
+ "version": "0.4.24",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `hasAgents` manifest injection: buildManifest appends the
3
+ * AgentRun/AgentMessage entities + owner-scoping policies when any
4
+ * `agent()` was discovered — as ordinary synced entities, so every
5
+ * downstream surface (sync, codegen, migrate) applies unchanged.
6
+ */
7
+ import { expect, test } from "bun:test";
8
+ import { buildManifest, entity, field } from "./index";
9
+
10
+ const base = {
11
+ name: "t",
12
+ version: "0",
13
+ entities: [entity("Doc", { title: field.string() })],
14
+ routes: [],
15
+ };
16
+
17
+ test("hasAgents injects entities, indexes, sync scopes, and policies", () => {
18
+ const m = buildManifest({ ...base, hasAgents: true });
19
+
20
+ const run = m.entities.find((e) => e.name === "AgentRun")!;
21
+ expect(run).toBeDefined();
22
+ const runFields = Object.fromEntries(run.fields.map((f) => [f.name, f.type]));
23
+ expect(runFields.agent).toBe("string");
24
+ expect(runFields.status).toBe("string");
25
+ expect(runFields.streamId).toBe("string");
26
+ expect(runFields.createdAt).toBe("datetime");
27
+ expect(run.sync_scope).toBe("auth.userId != null && data.userId == auth.userId");
28
+
29
+ const msg = m.entities.find((e) => e.name === "AgentMessage")!;
30
+ expect(msg).toBeDefined();
31
+ const msgFields = Object.fromEntries(msg.fields.map((f) => [f.name, f.type]));
32
+ expect(msgFields.runId).toBe("id(AgentRun)");
33
+ expect(msgFields.content).toBe("json");
34
+ expect(msgFields.seq).toBe("int");
35
+ expect(msg.indexes.some((i) => i.name === "by_run")).toBe(true);
36
+ expect(msg.sync_scope).toBe("auth.userId != null && data.userId == auth.userId");
37
+
38
+ const runPolicy = m.policies.find((p) => p.entity === "AgentRun")!;
39
+ expect(runPolicy.allowRead).toBe("auth.userId != null && data.userId == auth.userId");
40
+ expect(runPolicy.allowInsert).toBe("false");
41
+ expect(runPolicy.allowUpdate).toBe("false");
42
+ expect(runPolicy.allowDelete).toBe("false");
43
+ expect(m.policies.some((p) => p.entity === "AgentMessage")).toBe(true);
44
+ });
45
+
46
+ test("without hasAgents nothing is injected", () => {
47
+ const m = buildManifest(base);
48
+ expect(m.entities.some((e) => e.name === "AgentRun")).toBe(false);
49
+ expect(m.policies.some((p) => p.entity === "AgentRun")).toBe(false);
50
+ });
51
+
52
+ test("app-declared AgentRun and policies win over injection", () => {
53
+ const custom = entity("AgentRun", { custom: field.string() });
54
+ const m = buildManifest({
55
+ ...base,
56
+ entities: [...base.entities, custom],
57
+ policies: [
58
+ { name: "mine", entity: "AgentRun", allowRead: "auth.userId != null" },
59
+ ],
60
+ hasAgents: true,
61
+ });
62
+ const runs = m.entities.filter((e) => e.name === "AgentRun");
63
+ expect(runs).toHaveLength(1);
64
+ expect(runs[0].fields.map((f) => f.name)).toEqual(["custom"]);
65
+ const policies = m.policies.filter((p) => p.entity === "AgentRun");
66
+ expect(policies).toHaveLength(1);
67
+ expect(policies[0].name).toBe("mine");
68
+ // AgentMessage still injected (not declared by the app).
69
+ expect(m.entities.some((e) => e.name === "AgentMessage")).toBe(true);
70
+ });
71
+
72
+ test("hasAgents is inferred from actions carrying isAgent", () => {
73
+ const m = buildManifest({
74
+ ...base,
75
+ actions: [{ name: "helper", isAgent: true, fnType: "action" }],
76
+ });
77
+ expect(m.entities.some((e) => e.name === "AgentRun")).toBe(true);
78
+ // Explicit false wins over inference.
79
+ const off = buildManifest({
80
+ ...base,
81
+ actions: [{ name: "helper", isAgent: true, fnType: "action" }],
82
+ hasAgents: false,
83
+ });
84
+ expect(off.entities.some((e) => e.name === "AgentRun")).toBe(false);
85
+ });
package/src/index.ts CHANGED
@@ -579,6 +579,12 @@ export interface ActionDefinition {
579
579
  input?: InputFieldDefinition[];
580
580
  /** See `QueryDefinition.auth`. */
581
581
  auth?: string;
582
+ /** Set by `discoverFunctions` for `agent()` definitions —
583
+ * `buildManifest` injects the AgentRun/AgentMessage entities when
584
+ * any action carries it, so templates that pass `actions:
585
+ * fns.actions` (rather than spreading the whole result) still get
586
+ * the injection. Not serialized into the manifest. */
587
+ isAgent?: boolean;
582
588
  /**
583
589
  * Which write kind this was before the manifest's read/write split
584
590
  * collapsed mutations and actions into one bucket. Preserved so a
@@ -1449,6 +1455,9 @@ function argsToInput(
1449
1455
  export interface DiscoveredFunctions {
1450
1456
  queries: QueryDefinition[];
1451
1457
  actions: ActionDefinition[];
1458
+ /** True when any discovered function is an `agent()` — buildManifest
1459
+ * then injects the AgentRun/AgentMessage entities + policies. */
1460
+ hasAgents?: boolean;
1452
1461
  }
1453
1462
 
1454
1463
  /**
@@ -1519,6 +1528,7 @@ export async function discoverFunctions(opts?: {
1519
1528
 
1520
1529
  const queries: QueryDefinition[] = [];
1521
1530
  const actions: ActionDefinition[] = [];
1531
+ let hasAgents = false;
1522
1532
 
1523
1533
  for (const file of files) {
1524
1534
  const name = file.replace(/\.(ts|js)$/, "");
@@ -1547,17 +1557,25 @@ export async function discoverFunctions(opts?: {
1547
1557
  continue;
1548
1558
  }
1549
1559
  if (shape.internal === true) continue;
1560
+ const isAgent = shape.__pylonAgent === true;
1561
+ if (isAgent) hasAgents = true;
1550
1562
 
1551
1563
  const input = argsToInput(definition!.args);
1552
1564
  const auth = typeof shape.auth === "string" ? shape.auth : undefined;
1553
1565
  if (definition!.type === "query") {
1554
1566
  queries.push({ name, input, auth });
1555
1567
  } else {
1556
- actions.push({ name, input, auth, fnType: definition!.type });
1568
+ actions.push({
1569
+ name,
1570
+ input,
1571
+ auth,
1572
+ fnType: definition!.type,
1573
+ ...(isAgent ? { isAgent: true } : {}),
1574
+ });
1557
1575
  }
1558
1576
  }
1559
1577
 
1560
- return { queries, actions };
1578
+ return { queries, actions, hasAgents };
1561
1579
  }
1562
1580
 
1563
1581
  export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
@@ -1936,6 +1954,91 @@ function isManifestAuthConfig(
1936
1954
  return "trusted_origins" in cfg;
1937
1955
  }
1938
1956
 
1957
+ /** The framework entities behind `agent()`. Plain synced entities so
1958
+ * every existing surface (sync, policies, codegen, migrate) applies. */
1959
+ function agentEntities(): EntityDefinition[] {
1960
+ return [
1961
+ entity(
1962
+ "AgentRun",
1963
+ {
1964
+ agent: field.string(),
1965
+ status: field.string(),
1966
+ // User-scoped, deliberately NOT tenantId-scoped: a tenantId
1967
+ // field would trip TenantScopePlugin's auto-scoping and reject
1968
+ // callers with no active organization. Apps needing org-scoped
1969
+ // agent visibility declare their own AgentRun (injection
1970
+ // defers to app-declared entities).
1971
+ userId: field.string().optional(),
1972
+ title: field.string().optional(),
1973
+ // Resumable-stream id of the CURRENT generation — another
1974
+ // device attaches to live tokens via resumeStream(streamId).
1975
+ // Owner-scoped by policy, so it never leaks across users.
1976
+ streamId: field.string().optional(),
1977
+ error: field.string().optional(),
1978
+ createdAt: field.datetime(),
1979
+ updatedAt: field.datetime(),
1980
+ },
1981
+ {
1982
+ indexes: [
1983
+ { name: "by_user", fields: ["userId"], unique: false },
1984
+ ],
1985
+ // Replicate only the caller's own runs. The explicit
1986
+ // null-guard matters: the policy engine evaluates null == null
1987
+ // as TRUE, so without it an anonymous visitor could read any
1988
+ // run that somehow ended up ownerless.
1989
+ sync: { where: "auth.userId != null && data.userId == auth.userId" },
1990
+ },
1991
+ ),
1992
+ entity(
1993
+ "AgentMessage",
1994
+ {
1995
+ runId: field.id("AgentRun"),
1996
+ userId: field.string().optional(),
1997
+ seq: field.int(),
1998
+ role: field.string(),
1999
+ content: field.json(),
2000
+ createdAt: field.datetime(),
2001
+ },
2002
+ {
2003
+ indexes: [
2004
+ // Unique: the loop is the only writer and single-writer per
2005
+ // run; a racing duplicate fails loudly instead of garbling
2006
+ // the transcript.
2007
+ { name: "by_run", fields: ["runId", "seq"], unique: true },
2008
+ ],
2009
+ sync: { where: "auth.userId != null && data.userId == auth.userId" },
2010
+ },
2011
+ ),
2012
+ ];
2013
+ }
2014
+
2015
+ /** Owner-only reads; every client write denied — the agent loop's
2016
+ * internal mutations (server-side) are the only writer. */
2017
+ function agentPolicies(): PolicyDefinition[] {
2018
+ const lockdown = {
2019
+ allowInsert: "false",
2020
+ allowUpdate: "false",
2021
+ allowDelete: "false",
2022
+ };
2023
+ // The `auth.userId != null` guard is load-bearing: the policy
2024
+ // engine evaluates null == null as TRUE, so a bare equality would
2025
+ // hand ownerless rows to every anonymous caller.
2026
+ return [
2027
+ {
2028
+ name: "agent_run_owner",
2029
+ entity: "AgentRun",
2030
+ allowRead: "auth.userId != null && data.userId == auth.userId",
2031
+ ...lockdown,
2032
+ },
2033
+ {
2034
+ name: "agent_message_owner",
2035
+ entity: "AgentMessage",
2036
+ allowRead: "auth.userId != null && data.userId == auth.userId",
2037
+ ...lockdown,
2038
+ },
2039
+ ];
2040
+ }
2041
+
1939
2042
  export function buildManifest(options: {
1940
2043
  name: string;
1941
2044
  version: string;
@@ -1949,6 +2052,11 @@ export function buildManifest(options: {
1949
2052
  connections?: ManifestConnection[];
1950
2053
  crons?: ManifestCron[];
1951
2054
  fonts?: ManifestFont[];
2055
+ /** Set by `discoverFunctions()` (spread its result into this call).
2056
+ * When true, the framework's AgentRun/AgentMessage entities and
2057
+ * their owner-scoping policies are appended to the manifest —
2058
+ * unless the app already declares entities with those names. */
2059
+ hasAgents?: boolean;
1952
2060
  }): AppManifest {
1953
2061
  // Pull policies attached via the fluent `e.entity().policies(...)`
1954
2062
  // chain onto the top-level policies list. Without this, fluent
@@ -1977,11 +2085,32 @@ export function buildManifest(options: {
1977
2085
  });
1978
2086
  }
1979
2087
  const allPolicies = [...(options.policies ?? []), ...attached];
2088
+ // Agent run state: when any `agent()` exists, its durable state lives
2089
+ // in two ORDINARY synced entities — owner-scoped reads, client writes
2090
+ // denied (the loop's internal mutations are the only writer). Being
2091
+ // plain entities means sync replication, codegen types, `pylon
2092
+ // migrate`, and `db.useQuery("AgentMessage", ...)` all just work.
2093
+ const allEntities = [...options.entities];
2094
+ const hasAgents =
2095
+ options.hasAgents ??
2096
+ (options.actions ?? []).some((a) => a.isAgent === true);
2097
+ if (hasAgents) {
2098
+ const taken = new Set(allEntities.map((e) => e.name));
2099
+ for (const ent of agentEntities()) {
2100
+ if (!taken.has(ent.name)) allEntities.push(ent);
2101
+ }
2102
+ const declaredFor = new Set(
2103
+ allPolicies.map((p) => p.entity).filter(Boolean),
2104
+ );
2105
+ for (const pol of agentPolicies()) {
2106
+ if (!declaredFor.has(pol.entity)) allPolicies.push(pol);
2107
+ }
2108
+ }
1980
2109
  return {
1981
2110
  manifest_version: MANIFEST_VERSION,
1982
2111
  name: options.name,
1983
2112
  version: options.version,
1984
- entities: entitiesToManifest(options.entities),
2113
+ entities: entitiesToManifest(allEntities),
1985
2114
  routes: routesToManifest(options.routes),
1986
2115
  queries: queriesToManifest(options.queries ?? []),
1987
2116
  actions: actionsToManifest(options.actions ?? []),