@happyvertical/smrt-app-shell 0.0.0-stage → 0.60.17
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/AGENTS.md +99 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +173 -2
- package/agents/engine.md +85 -0
- package/dist/engine/app-runtime.d.ts +138 -0
- package/dist/engine/app-runtime.js +545 -0
- package/dist/engine/app-worker.d.ts +1 -0
- package/dist/engine/app-worker.js +62 -0
- package/dist/engine/commands.d.ts +94 -0
- package/dist/engine/commands.js +30 -0
- package/dist/engine/definition.d.ts +192 -0
- package/dist/engine/definition.js +497 -0
- package/dist/engine/errors.d.ts +23 -0
- package/dist/engine/errors.js +35 -0
- package/dist/engine/host.d.ts +153 -0
- package/dist/engine/host.js +791 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/sandbox.d.ts +60 -0
- package/dist/engine/sandbox.js +260 -0
- package/dist/engine/snapshots.d.ts +7 -0
- package/dist/engine/snapshots.js +54 -0
- package/dist/engine/store.d.ts +39 -0
- package/dist/engine/store.js +32 -0
- package/dist/engine/worker-handle.d.ts +29 -0
- package/dist/engine/worker-handle.js +89 -0
- package/dist/engine/worker-protocol.d.ts +22 -0
- package/dist/engine/worker-protocol.js +20 -0
- package/dist/lib/objects/App.d.ts +60 -0
- package/dist/lib/objects/App.js +273 -0
- package/dist/lib/objects/AppVersion.d.ts +22 -0
- package/dist/lib/objects/AppVersion.js +62 -0
- package/dist/lib/objects/Approval.d.ts +39 -0
- package/dist/lib/objects/Approval.js +101 -0
- package/dist/lib/objects/AssistantMessage.d.ts +24 -0
- package/dist/lib/objects/AssistantMessage.js +63 -0
- package/dist/lib/objects/AssistantThread.d.ts +16 -0
- package/dist/lib/objects/AssistantThread.js +44 -0
- package/dist/lib/objects/index.d.ts +5 -0
- package/dist/lib/objects/index.js +5 -0
- package/dist/lib/server/approvals.d.ts +69 -0
- package/dist/lib/server/approvals.js +239 -0
- package/dist/lib/server/builder.d.ts +16 -0
- package/dist/lib/server/builder.js +71 -0
- package/dist/lib/server/events.d.ts +16 -0
- package/dist/lib/server/events.js +14 -0
- package/dist/lib/server/host.d.ts +22 -0
- package/dist/lib/server/host.js +96 -0
- package/dist/lib/server/owner.d.ts +33 -0
- package/dist/lib/server/owner.js +92 -0
- package/dist/lib/server/version-store.d.ts +20 -0
- package/dist/lib/server/version-store.js +115 -0
- package/dist/manifest.json +1680 -0
- package/dist/smrt-knowledge.okf.md +95 -0
- package/package.json +80 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Who may do what, in one place. The shell has exactly one person: the owner.
|
|
2
|
+
// Every builder operation, approval decision and route requires the owner's
|
|
3
|
+
// session; the engine's `approved` flag is reachable only from
|
|
4
|
+
// `Approval.decide` (see approvals.ts).
|
|
5
|
+
//
|
|
6
|
+
// Owner = an ACTIVE membership holding the built-in system `owner` role in the
|
|
7
|
+
// principal's tenant, looked up in shell.db on every check (no claim in a
|
|
8
|
+
// token, header or request body is trusted).
|
|
9
|
+
import { getCurrentTenant, withTenant, } from '@happyvertical/smrt-tenancy';
|
|
10
|
+
import { DEFAULT_ROLE_SLUGS, MembershipCollection, MembershipStatus, RoleCollection, } from '@happyvertical/smrt-users';
|
|
11
|
+
import { getShellDb } from "./host.js";
|
|
12
|
+
export class PolicyError extends Error {
|
|
13
|
+
status;
|
|
14
|
+
constructor(status, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'PolicyError';
|
|
17
|
+
this.status = status;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function ownerFromDatabase(principal) {
|
|
21
|
+
const db = await getShellDb();
|
|
22
|
+
const roles = await RoleCollection.create({ db });
|
|
23
|
+
const ownerRole = await roles.findSystemRoleBySlug(DEFAULT_ROLE_SLUGS.OWNER);
|
|
24
|
+
if (!ownerRole?.id)
|
|
25
|
+
return false;
|
|
26
|
+
const memberships = await MembershipCollection.create({ db });
|
|
27
|
+
const membership = await memberships.findByUserAndTenant(principal.id, principal.tenantId);
|
|
28
|
+
return (!!membership &&
|
|
29
|
+
membership.status === MembershipStatus.ACTIVE &&
|
|
30
|
+
membership.roleId === ownerRole.id);
|
|
31
|
+
}
|
|
32
|
+
let override = null;
|
|
33
|
+
/** Tests only: replace the database lookup. Pass null to restore it. */
|
|
34
|
+
export function setOwnerCheck(check) {
|
|
35
|
+
override = check;
|
|
36
|
+
}
|
|
37
|
+
export async function isOwner(principal) {
|
|
38
|
+
if (!principal?.id || !principal.tenantId)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
return await (override ?? ownerFromDatabase)(principal);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false; // fail closed
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The principal, or a 401 (nobody) / 403 (somebody who is not the owner). */
|
|
48
|
+
export async function requireOwner(principal) {
|
|
49
|
+
if (!principal?.id || !principal.tenantId)
|
|
50
|
+
throw new PolicyError(401, 'sign in first');
|
|
51
|
+
if (!(await isOwner(principal)))
|
|
52
|
+
throw new PolicyError(403, 'only the owner can do this');
|
|
53
|
+
return principal;
|
|
54
|
+
}
|
|
55
|
+
/** The principal of a SvelteKit request, from the verified session only. */
|
|
56
|
+
export function principalFromLocals(locals) {
|
|
57
|
+
const id = locals.user?.id;
|
|
58
|
+
if (typeof id !== 'string' || !id)
|
|
59
|
+
return null;
|
|
60
|
+
if (typeof locals.tenantId !== 'string' || !locals.tenantId)
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
tenantId: locals.tenantId,
|
|
65
|
+
scopes: Array.isArray(locals.permissions)
|
|
66
|
+
? locals.permissions
|
|
67
|
+
: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The principal of the running async scope (the session's tenant context). */
|
|
71
|
+
export function currentPrincipal() {
|
|
72
|
+
const ctx = getCurrentTenant();
|
|
73
|
+
if (!ctx?.userId || !ctx.tenantId)
|
|
74
|
+
return null;
|
|
75
|
+
return { id: ctx.userId, tenantId: ctx.tenantId };
|
|
76
|
+
}
|
|
77
|
+
/** For model methods: the owner of the current scope, or a refusal. */
|
|
78
|
+
export function assertOwner() {
|
|
79
|
+
return requireOwner(currentPrincipal());
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run `fn` inside the verified principal's tenant context, so the owner check
|
|
83
|
+
* in model methods (`assertOwner`) sees exactly the principal a route
|
|
84
|
+
* verified, whatever async context the route runs in.
|
|
85
|
+
*/
|
|
86
|
+
export function runAs(principal, fn) {
|
|
87
|
+
return withTenant({
|
|
88
|
+
tenantId: principal.tenantId,
|
|
89
|
+
userId: principal.id,
|
|
90
|
+
permissions: new Set(principal.scopes ?? []),
|
|
91
|
+
}, fn);
|
|
92
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import type { VersionRecord, VersionStore } from '../../engine/index.ts';
|
|
3
|
+
import { type App } from '../objects/App.ts';
|
|
4
|
+
export declare const APP_SLUG = "app";
|
|
5
|
+
export declare class ShellVersionStore implements VersionStore {
|
|
6
|
+
private readonly db;
|
|
7
|
+
constructor(db: DatabaseInterface);
|
|
8
|
+
private versions;
|
|
9
|
+
private apps;
|
|
10
|
+
private row;
|
|
11
|
+
/** The singleton App row, created on first use. */
|
|
12
|
+
appRow(): Promise<App>;
|
|
13
|
+
current(): Promise<number | null>;
|
|
14
|
+
setCurrent(version: number): Promise<void>;
|
|
15
|
+
get(version: number): Promise<VersionRecord | null>;
|
|
16
|
+
list(): Promise<VersionRecord[]>;
|
|
17
|
+
append(record: VersionRecord): Promise<void>;
|
|
18
|
+
markPurged(version: number): Promise<void>;
|
|
19
|
+
maxVersion(): Promise<number>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AppCollection } from "../objects/App.js";
|
|
2
|
+
import { AppVersionCollection, } from "../objects/AppVersion.js";
|
|
3
|
+
export const APP_SLUG = 'app';
|
|
4
|
+
function toRecord(row) {
|
|
5
|
+
const migration = row.migration
|
|
6
|
+
? JSON.parse(row.migration)
|
|
7
|
+
: null;
|
|
8
|
+
if (migration && row.purged)
|
|
9
|
+
migration.purged = true;
|
|
10
|
+
return {
|
|
11
|
+
version: Number(row.version),
|
|
12
|
+
parent: Number(row.parent) < 0 ? null : Number(row.parent),
|
|
13
|
+
author: row.author,
|
|
14
|
+
summary: row.summary,
|
|
15
|
+
definition: JSON.parse(row.definition),
|
|
16
|
+
migration,
|
|
17
|
+
// the ORM may hydrate a timestamp-looking column as a Date
|
|
18
|
+
createdAt: row.createdAt instanceof Date
|
|
19
|
+
? row.createdAt.toISOString()
|
|
20
|
+
: String(row.createdAt),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export class ShellVersionStore {
|
|
24
|
+
db;
|
|
25
|
+
constructor(db) {
|
|
26
|
+
this.db = db;
|
|
27
|
+
}
|
|
28
|
+
versions() {
|
|
29
|
+
return AppVersionCollection.create({ db: this.db });
|
|
30
|
+
}
|
|
31
|
+
apps() {
|
|
32
|
+
return AppCollection.create({ db: this.db });
|
|
33
|
+
}
|
|
34
|
+
async row(version) {
|
|
35
|
+
const rows = await (await this.versions()).list({
|
|
36
|
+
where: { version },
|
|
37
|
+
limit: 1,
|
|
38
|
+
});
|
|
39
|
+
return rows[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
/** The singleton App row, created on first use. */
|
|
42
|
+
async appRow() {
|
|
43
|
+
const apps = await this.apps();
|
|
44
|
+
const rows = await apps.list({ where: { slug: APP_SLUG }, limit: 1 });
|
|
45
|
+
if (rows[0])
|
|
46
|
+
return rows[0];
|
|
47
|
+
const created = await apps.create({
|
|
48
|
+
slug: APP_SLUG,
|
|
49
|
+
name: 'My app',
|
|
50
|
+
currentVersion: -1,
|
|
51
|
+
definition: '',
|
|
52
|
+
});
|
|
53
|
+
await created.save();
|
|
54
|
+
return created;
|
|
55
|
+
}
|
|
56
|
+
async current() {
|
|
57
|
+
const v = Number((await this.appRow()).currentVersion);
|
|
58
|
+
return v < 0 ? null : v;
|
|
59
|
+
}
|
|
60
|
+
async setCurrent(version) {
|
|
61
|
+
const rec = await this.row(version);
|
|
62
|
+
if (!rec)
|
|
63
|
+
throw new Error(`version ${version} is not stored`);
|
|
64
|
+
const app = await this.appRow();
|
|
65
|
+
app.currentVersion = version;
|
|
66
|
+
app.definition = rec.definition;
|
|
67
|
+
try {
|
|
68
|
+
app.name = JSON.parse(rec.definition).name ?? app.name;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* keep the name */
|
|
72
|
+
}
|
|
73
|
+
await app.save();
|
|
74
|
+
}
|
|
75
|
+
async get(version) {
|
|
76
|
+
const row = await this.row(version);
|
|
77
|
+
return row ? toRecord(row) : null;
|
|
78
|
+
}
|
|
79
|
+
async list() {
|
|
80
|
+
const rows = await (await this.versions()).list({ limit: 100000 });
|
|
81
|
+
return rows.map(toRecord).sort((a, b) => a.version - b.version);
|
|
82
|
+
}
|
|
83
|
+
async append(record) {
|
|
84
|
+
if (await this.row(record.version))
|
|
85
|
+
throw new Error(`version ${record.version} already exists`);
|
|
86
|
+
const created = await (await this.versions()).create({
|
|
87
|
+
version: record.version,
|
|
88
|
+
parent: record.parent ?? -1,
|
|
89
|
+
author: record.author,
|
|
90
|
+
summary: record.summary,
|
|
91
|
+
definition: JSON.stringify(record.definition),
|
|
92
|
+
migration: record.migration ? JSON.stringify(record.migration) : '',
|
|
93
|
+
createdAt: record.createdAt,
|
|
94
|
+
purged: !!record.migration?.purged,
|
|
95
|
+
});
|
|
96
|
+
await created.save();
|
|
97
|
+
}
|
|
98
|
+
async markPurged(version) {
|
|
99
|
+
const row = await this.row(version);
|
|
100
|
+
if (!row)
|
|
101
|
+
return;
|
|
102
|
+
row.purged = true;
|
|
103
|
+
if (row.migration) {
|
|
104
|
+
row.migration = JSON.stringify({
|
|
105
|
+
...JSON.parse(row.migration),
|
|
106
|
+
purged: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
await row.save();
|
|
110
|
+
}
|
|
111
|
+
async maxVersion() {
|
|
112
|
+
const rows = await (await this.versions()).list({ limit: 100000 });
|
|
113
|
+
return rows.length ? Math.max(...rows.map((r) => Number(r.version))) : -1;
|
|
114
|
+
}
|
|
115
|
+
}
|