@pylonsync/sdk 0.4.22 → 0.4.23
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 +14 -0
- package/package.json +1 -1
- package/src/agent-manifest.test.ts +85 -0
- package/src/index.ts +132 -3
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
|
@@ -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({
|
|
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(
|
|
2113
|
+
entities: entitiesToManifest(allEntities),
|
|
1985
2114
|
routes: routesToManifest(options.routes),
|
|
1986
2115
|
queries: queriesToManifest(options.queries ?? []),
|
|
1987
2116
|
actions: actionsToManifest(options.actions ?? []),
|