pilotswarm-sdk 0.5.44 → 0.5.46
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/api/src/api-client.js +6 -1
- package/api/src/http-api-transport.js +47 -0
- package/api/src/protocol.js +14 -0
- package/dist/agent-manager-tools.d.ts +2 -0
- package/dist/agent-manager-tools.d.ts.map +1 -1
- package/dist/agent-manager-tools.js +4 -1
- package/dist/agent-manager-tools.js.map +1 -1
- package/dist/agent-package-format.d.ts +6 -0
- package/dist/agent-package-format.d.ts.map +1 -1
- package/dist/agent-package-format.js +33 -0
- package/dist/agent-package-format.js.map +1 -1
- package/dist/agent-package-service.d.ts.map +1 -1
- package/dist/agent-package-service.js +7 -2
- package/dist/agent-package-service.js.map +1 -1
- package/dist/artifact-tools.js +1 -1
- package/dist/artifact-tools.js.map +1 -1
- package/dist/canvas-app-catalog.d.ts +61 -0
- package/dist/canvas-app-catalog.d.ts.map +1 -0
- package/dist/canvas-app-catalog.js +146 -0
- package/dist/canvas-app-catalog.js.map +1 -0
- package/dist/canvas-app-manifest.d.ts +94 -2
- package/dist/canvas-app-manifest.d.ts.map +1 -1
- package/dist/canvas-app-manifest.js +140 -1
- package/dist/canvas-app-manifest.js.map +1 -1
- package/dist/canvas-kv.d.ts +176 -0
- package/dist/canvas-kv.d.ts.map +1 -0
- package/dist/canvas-kv.js +281 -0
- package/dist/canvas-kv.js.map +1 -0
- package/dist/canvas-support.d.ts +40 -0
- package/dist/canvas-support.d.ts.map +1 -0
- package/dist/canvas-support.js +79 -0
- package/dist/canvas-support.js.map +1 -0
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +822 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +101 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +80 -1
- package/dist/cms.js.map +1 -1
- package/dist/facts-migrations.d.ts.map +1 -1
- package/dist/facts-migrations.js +30 -0
- package/dist/facts-migrations.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect-tools.d.ts +6 -0
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +1 -0
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts +7 -0
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +193 -64
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +37 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +80 -3
- package/dist/management-client.js.map +1 -1
- package/dist/mcp-loader.d.ts +68 -0
- package/dist/mcp-loader.d.ts.map +1 -1
- package/dist/mcp-loader.js +79 -0
- package/dist/mcp-loader.js.map +1 -1
- package/dist/session-manager.d.ts +20 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +13 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +1 -9
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +95 -77
- package/dist/session-proxy.js.map +1 -1
- package/dist/web/generated-op-methods.d.ts +53 -0
- package/dist/web/generated-op-methods.d.ts.map +1 -1
- package/dist/web/generated-op-methods.js +12 -0
- package/dist/web/generated-op-methods.js.map +1 -1
- package/dist/web/web-management-client.d.ts +18 -0
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +27 -0
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +27 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +145 -13
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/system/agents/default.agent.md +30 -126
- package/plugins/system/skills/canvas-apps/SKILL.md +224 -111
- package/plugins/system/skills/html-visuals/SKILL.md +2 -2
package/dist/cms-migrations.js
CHANGED
|
@@ -322,6 +322,16 @@ export function CMS_MIGRATIONS(schema) {
|
|
|
322
322
|
name: "provider_ledger_base",
|
|
323
323
|
sql: migration_0062_provider_ledger_base(schema),
|
|
324
324
|
},
|
|
325
|
+
{
|
|
326
|
+
version: "0063",
|
|
327
|
+
name: "agent_package_editors",
|
|
328
|
+
sql: migration_0063_agent_package_editors(schema),
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
version: "0064",
|
|
332
|
+
name: "canvas_kv",
|
|
333
|
+
sql: migration_0064_canvas_kv(schema),
|
|
334
|
+
},
|
|
325
335
|
];
|
|
326
336
|
}
|
|
327
337
|
// ─── Migration 0044: ms-space keyset for session paging ──────────
|
|
@@ -12747,4 +12757,816 @@ END;
|
|
|
12747
12757
|
$$ LANGUAGE plpgsql VOLATILE;
|
|
12748
12758
|
`;
|
|
12749
12759
|
}
|
|
12760
|
+
// ─── Migration 0063: package editors, and everyone in the picker ─────
|
|
12761
|
+
//
|
|
12762
|
+
// Two things, one migration, because they ship together.
|
|
12763
|
+
//
|
|
12764
|
+
// EDITORS. A shared package's owner can grant named users WRITE access:
|
|
12765
|
+
// publish a new version, republish into it, pin, enable/disable. Never
|
|
12766
|
+
// scope changes, delete, or the editor list itself — those stay with the
|
|
12767
|
+
// owner (or an admin). The grant lives on the SHARED row only, and it is
|
|
12768
|
+
// deleted when that row is demoted to user scope: unsharing revokes. A
|
|
12769
|
+
// personal copy is a different row and never inherits a grant.
|
|
12770
|
+
//
|
|
12771
|
+
// The table keys on users.user_id like session_shares does, and the grant
|
|
12772
|
+
// path creates the grantee row create-only (0033's rule): a person who has
|
|
12773
|
+
// never signed in can be granted, and the 0032 email adoption folds them
|
|
12774
|
+
// into the real principal on first login.
|
|
12775
|
+
//
|
|
12776
|
+
// THE PICKER. cms_list_users hid every row whose display_name was NULL —
|
|
12777
|
+
// which is every Entra sign-in whose access token carries no `name` claim.
|
|
12778
|
+
// The user exists (0042 registers them at login) and is invisible to every
|
|
12779
|
+
// share dialog. Widen to "has a name OR an email"; a row with neither is
|
|
12780
|
+
// still the raw-id-grant placeholder the original filter meant to hide.
|
|
12781
|
+
//
|
|
12782
|
+
// Every function below that already existed is CREATE OR REPLACE'd here in
|
|
12783
|
+
// full: editing an applied migration in place is a silent no-op.
|
|
12784
|
+
function migration_0063_agent_package_editors(schema) {
|
|
12785
|
+
const s = `"${schema}"`;
|
|
12786
|
+
return `
|
|
12787
|
+
CREATE TABLE IF NOT EXISTS ${s}.agent_package_editors (
|
|
12788
|
+
package_id TEXT NOT NULL REFERENCES ${s}.agent_packages(package_id) ON DELETE CASCADE,
|
|
12789
|
+
user_id BIGINT NOT NULL REFERENCES ${s}.users(user_id),
|
|
12790
|
+
granted_by BIGINT REFERENCES ${s}.users(user_id),
|
|
12791
|
+
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12792
|
+
PRIMARY KEY (package_id, user_id)
|
|
12793
|
+
);
|
|
12794
|
+
CREATE INDEX IF NOT EXISTS agent_package_editors_user_idx ON ${s}.agent_package_editors (user_id);
|
|
12795
|
+
|
|
12796
|
+
-- Is this principal an editor of this package row?
|
|
12797
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_agent_package_is_editor(
|
|
12798
|
+
p_package_id TEXT, p_provider TEXT, p_subject TEXT
|
|
12799
|
+
) RETURNS BOOLEAN AS $$
|
|
12800
|
+
SELECT EXISTS (
|
|
12801
|
+
SELECT 1
|
|
12802
|
+
FROM ${s}.agent_package_editors e
|
|
12803
|
+
JOIN ${s}.users u ON u.user_id = e.user_id
|
|
12804
|
+
WHERE e.package_id = p_package_id
|
|
12805
|
+
AND u.provider = NULLIF(BTRIM(p_provider), '')
|
|
12806
|
+
AND u.subject = NULLIF(BTRIM(p_subject), '')
|
|
12807
|
+
);
|
|
12808
|
+
$$ LANGUAGE sql STABLE;
|
|
12809
|
+
|
|
12810
|
+
-- ── Authz gains an editor mode ───────────────────────────────────
|
|
12811
|
+
--
|
|
12812
|
+
-- One new trailing argument with a default, so the existing seven-argument
|
|
12813
|
+
-- callers (delete, and anything else that must stay owner-only) keep
|
|
12814
|
+
-- resolving to this function unchanged. The seven-argument overload is
|
|
12815
|
+
-- dropped first: leaving it would make every seven-argument call ambiguous.
|
|
12816
|
+
DROP FUNCTION IF EXISTS ${s}.cms_agent_package_authz(TEXT, TEXT, TEXT, BOOLEAN, TEXT, TEXT, TEXT);
|
|
12817
|
+
|
|
12818
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_agent_package_authz(
|
|
12819
|
+
p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
|
|
12820
|
+
p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT,
|
|
12821
|
+
p_allow_editor BOOLEAN DEFAULT FALSE
|
|
12822
|
+
) RETURNS ${s}.agent_packages AS $$
|
|
12823
|
+
DECLARE
|
|
12824
|
+
v_pkg ${s}.agent_packages;
|
|
12825
|
+
v_id TEXT;
|
|
12826
|
+
v_count INT;
|
|
12827
|
+
v_is_owner BOOLEAN;
|
|
12828
|
+
BEGIN
|
|
12829
|
+
v_id := ${s}.cms_resolve_agent_package_id(
|
|
12830
|
+
p_name, p_actor_provider, p_actor_subject,
|
|
12831
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, FALSE);
|
|
12832
|
+
|
|
12833
|
+
-- Editors do not own a copy, so "my copy, then shared" finds nothing for
|
|
12834
|
+
-- them when they omit the selector. Fall through to the shared copy,
|
|
12835
|
+
-- which is the only row an editor grant can exist on.
|
|
12836
|
+
IF v_id IS NULL AND p_allow_editor AND p_sel_scope IS NULL THEN
|
|
12837
|
+
SELECT p.package_id INTO v_id FROM ${s}.agent_packages p
|
|
12838
|
+
WHERE p.name = p_name AND p.scope = 'shared';
|
|
12839
|
+
END IF;
|
|
12840
|
+
|
|
12841
|
+
-- Admin fallback (0043): NULL-owner rows and other users' private rows
|
|
12842
|
+
-- have no owner triple to select by; refuse ambiguity rather than guess.
|
|
12843
|
+
IF v_id IS NULL AND p_is_admin AND p_sel_scope IS NULL THEN
|
|
12844
|
+
SELECT count(*) INTO v_count FROM ${s}.agent_packages p WHERE p.name = p_name;
|
|
12845
|
+
IF v_count > 1 THEN
|
|
12846
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_AMBIGUOUS: % copies of "%" exist; name an owner or scope to pick one', v_count, p_name;
|
|
12847
|
+
END IF;
|
|
12848
|
+
SELECT p.package_id INTO v_id FROM ${s}.agent_packages p WHERE p.name = p_name;
|
|
12849
|
+
END IF;
|
|
12850
|
+
|
|
12851
|
+
IF v_id IS NULL THEN
|
|
12852
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
|
|
12853
|
+
END IF;
|
|
12854
|
+
|
|
12855
|
+
SELECT * INTO v_pkg FROM ${s}.agent_packages WHERE package_id = v_id FOR UPDATE;
|
|
12856
|
+
IF NOT FOUND THEN
|
|
12857
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
|
|
12858
|
+
END IF;
|
|
12859
|
+
|
|
12860
|
+
v_is_owner := v_pkg.owner_provider IS NOT NULL
|
|
12861
|
+
AND v_pkg.owner_provider IS NOT DISTINCT FROM NULLIF(BTRIM(p_actor_provider), '')
|
|
12862
|
+
AND v_pkg.owner_subject IS NOT DISTINCT FROM NULLIF(BTRIM(p_actor_subject), '');
|
|
12863
|
+
|
|
12864
|
+
IF p_is_admin OR v_is_owner THEN
|
|
12865
|
+
RETURN v_pkg;
|
|
12866
|
+
END IF;
|
|
12867
|
+
IF p_allow_editor AND ${s}.cms_agent_package_is_editor(v_pkg.package_id, p_actor_provider, p_actor_subject) THEN
|
|
12868
|
+
RETURN v_pkg;
|
|
12869
|
+
END IF;
|
|
12870
|
+
IF p_allow_editor THEN
|
|
12871
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator, an editor, or an admin can modify "%"', p_name;
|
|
12872
|
+
END IF;
|
|
12873
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator or an admin can modify "%"', p_name;
|
|
12874
|
+
END;
|
|
12875
|
+
$$ LANGUAGE plpgsql;
|
|
12876
|
+
|
|
12877
|
+
-- ── Editor-level mutations: enable/disable and pin ───────────────
|
|
12878
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_enabled(
|
|
12879
|
+
p_name TEXT, p_enabled BOOLEAN, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
|
|
12880
|
+
p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
|
|
12881
|
+
) RETURNS VOID AS $$
|
|
12882
|
+
DECLARE
|
|
12883
|
+
v_pkg ${s}.agent_packages;
|
|
12884
|
+
BEGIN
|
|
12885
|
+
v_pkg := ${s}.cms_agent_package_authz(
|
|
12886
|
+
p_name, p_actor_provider, p_actor_subject, p_is_admin,
|
|
12887
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, TRUE);
|
|
12888
|
+
UPDATE ${s}.agent_packages SET enabled = p_enabled WHERE package_id = v_pkg.package_id;
|
|
12889
|
+
PERFORM ${s}.cms_agent_registry_bump();
|
|
12890
|
+
END;
|
|
12891
|
+
$$ LANGUAGE plpgsql;
|
|
12892
|
+
|
|
12893
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_pin_agent_package_version(
|
|
12894
|
+
p_name TEXT, p_semver TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
|
|
12895
|
+
p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
|
|
12896
|
+
) RETURNS VOID AS $$
|
|
12897
|
+
DECLARE
|
|
12898
|
+
v_pkg ${s}.agent_packages;
|
|
12899
|
+
v_version_id TEXT;
|
|
12900
|
+
BEGIN
|
|
12901
|
+
v_pkg := ${s}.cms_agent_package_authz(
|
|
12902
|
+
p_name, p_actor_provider, p_actor_subject, p_is_admin,
|
|
12903
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, TRUE);
|
|
12904
|
+
SELECT version_id INTO v_version_id FROM ${s}.agent_package_versions
|
|
12905
|
+
WHERE package_id = v_pkg.package_id AND semver = p_semver;
|
|
12906
|
+
IF NOT FOUND THEN
|
|
12907
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_VERSION_NOT_FOUND: %@% is not a published version', p_name, p_semver;
|
|
12908
|
+
END IF;
|
|
12909
|
+
UPDATE ${s}.agent_packages SET active_version_id = v_version_id WHERE package_id = v_pkg.package_id;
|
|
12910
|
+
PERFORM ${s}.cms_agent_registry_bump();
|
|
12911
|
+
END;
|
|
12912
|
+
$$ LANGUAGE plpgsql;
|
|
12913
|
+
|
|
12914
|
+
-- ── Scope change stays owner-only; demotion revokes every editor ─
|
|
12915
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_scope(
|
|
12916
|
+
p_name TEXT, p_scope TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
|
|
12917
|
+
p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
|
|
12918
|
+
) RETURNS VOID AS $$
|
|
12919
|
+
DECLARE
|
|
12920
|
+
v_pkg ${s}.agent_packages;
|
|
12921
|
+
v_clash TEXT;
|
|
12922
|
+
BEGIN
|
|
12923
|
+
IF p_scope NOT IN ('shared', 'user') THEN
|
|
12924
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_BAD_SCOPE: scope must be shared or user, got "%"', p_scope;
|
|
12925
|
+
END IF;
|
|
12926
|
+
v_pkg := ${s}.cms_agent_package_authz(
|
|
12927
|
+
p_name, p_actor_provider, p_actor_subject, p_is_admin,
|
|
12928
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject);
|
|
12929
|
+
|
|
12930
|
+
IF p_scope = 'shared' AND v_pkg.scope <> 'shared' THEN
|
|
12931
|
+
SELECT p.package_id INTO v_clash FROM ${s}.agent_packages p
|
|
12932
|
+
WHERE p.name = p_name AND p.scope = 'shared';
|
|
12933
|
+
IF v_clash IS NOT NULL THEN
|
|
12934
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NAME_TAKEN: a shared package named "%" already exists; rename before promoting', p_name;
|
|
12935
|
+
END IF;
|
|
12936
|
+
END IF;
|
|
12937
|
+
|
|
12938
|
+
IF p_scope = 'user' AND v_pkg.scope <> 'user' THEN
|
|
12939
|
+
IF v_pkg.owner_provider IS NULL OR v_pkg.owner_subject IS NULL THEN
|
|
12940
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NO_OWNER: "%" has no owner identity to demote to', p_name;
|
|
12941
|
+
END IF;
|
|
12942
|
+
SELECT p.package_id INTO v_clash FROM ${s}.agent_packages p
|
|
12943
|
+
WHERE p.name = p_name AND p.scope = 'user'
|
|
12944
|
+
AND p.owner_provider = v_pkg.owner_provider
|
|
12945
|
+
AND p.owner_subject = v_pkg.owner_subject;
|
|
12946
|
+
IF v_clash IS NOT NULL THEN
|
|
12947
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NAME_TAKEN: that owner already has a user-scope package named "%"', p_name;
|
|
12948
|
+
END IF;
|
|
12949
|
+
-- Unsharing revokes. Same statement, same transaction: a failed
|
|
12950
|
+
-- demotion above leaves the grants exactly as they were.
|
|
12951
|
+
DELETE FROM ${s}.agent_package_editors WHERE package_id = v_pkg.package_id;
|
|
12952
|
+
END IF;
|
|
12953
|
+
|
|
12954
|
+
UPDATE ${s}.agent_packages SET scope = p_scope WHERE package_id = v_pkg.package_id;
|
|
12955
|
+
PERFORM ${s}.cms_agent_registry_bump();
|
|
12956
|
+
END;
|
|
12957
|
+
$$ LANGUAGE plpgsql;
|
|
12958
|
+
|
|
12959
|
+
-- ── Publish: an editor may add versions to the shared package ────
|
|
12960
|
+
--
|
|
12961
|
+
-- Body is 0043's, with one change at the existing-package gate. The row
|
|
12962
|
+
-- keeps its owner: an editor's publish never reassigns the package.
|
|
12963
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_publish_agent_package(
|
|
12964
|
+
p_package_id TEXT, p_version_id TEXT, p_name TEXT, p_scope TEXT,
|
|
12965
|
+
p_owner_provider TEXT, p_owner_subject TEXT, p_source_id TEXT,
|
|
12966
|
+
p_semver TEXT, p_sha256 TEXT, p_size_bytes BIGINT, p_artifact_filename TEXT,
|
|
12967
|
+
p_commit_sha TEXT, p_manifest JSONB, p_created_by TEXT, p_is_admin BOOLEAN
|
|
12968
|
+
) RETURNS TABLE(status TEXT, package_id TEXT, version_id TEXT) AS $$
|
|
12969
|
+
DECLARE
|
|
12970
|
+
v_pkg RECORD;
|
|
12971
|
+
v_ver RECORD;
|
|
12972
|
+
v_owner_provider TEXT := NULLIF(BTRIM(p_owner_provider), '');
|
|
12973
|
+
v_owner_subject TEXT := NULLIF(BTRIM(p_owner_subject), '');
|
|
12974
|
+
v_is_owner BOOLEAN;
|
|
12975
|
+
BEGIN
|
|
12976
|
+
IF p_scope NOT IN ('shared', 'user') THEN
|
|
12977
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_BAD_SCOPE: scope must be shared or user, got "%"', p_scope;
|
|
12978
|
+
END IF;
|
|
12979
|
+
|
|
12980
|
+
IF LOWER(BTRIM(p_name)) LIKE '\\_\\_%' THEN
|
|
12981
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_RESERVED_NAME: "%" uses the reserved "__" prefix', p_name;
|
|
12982
|
+
END IF;
|
|
12983
|
+
|
|
12984
|
+
IF NOT p_is_admin AND (v_owner_provider IS NULL OR v_owner_subject IS NULL) THEN
|
|
12985
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: publishing without an owner identity requires the admin role';
|
|
12986
|
+
END IF;
|
|
12987
|
+
|
|
12988
|
+
<<retry>>
|
|
12989
|
+
LOOP
|
|
12990
|
+
SELECT * INTO v_pkg FROM ${s}.agent_packages p
|
|
12991
|
+
WHERE p.name = p_name
|
|
12992
|
+
AND p.scope = p_scope
|
|
12993
|
+
AND (p_scope = 'shared'
|
|
12994
|
+
OR (p.owner_provider IS NOT DISTINCT FROM v_owner_provider
|
|
12995
|
+
AND p.owner_subject IS NOT DISTINCT FROM v_owner_subject))
|
|
12996
|
+
FOR UPDATE;
|
|
12997
|
+
IF NOT FOUND THEN
|
|
12998
|
+
BEGIN
|
|
12999
|
+
INSERT INTO ${s}.agent_packages
|
|
13000
|
+
(package_id, source_id, name, scope, owner_provider, owner_subject, created_by)
|
|
13001
|
+
VALUES (p_package_id, p_source_id, p_name, p_scope,
|
|
13002
|
+
v_owner_provider, v_owner_subject, p_created_by);
|
|
13003
|
+
EXCEPTION WHEN unique_violation THEN
|
|
13004
|
+
CONTINUE retry;
|
|
13005
|
+
END;
|
|
13006
|
+
INSERT INTO ${s}.agent_package_versions
|
|
13007
|
+
(version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
|
|
13008
|
+
VALUES (p_version_id, p_package_id, p_semver, p_sha256, p_size_bytes,
|
|
13009
|
+
p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
|
|
13010
|
+
UPDATE ${s}.agent_packages SET active_version_id = p_version_id WHERE ${s}.agent_packages.package_id = p_package_id;
|
|
13011
|
+
PERFORM ${s}.cms_agent_registry_bump();
|
|
13012
|
+
RETURN QUERY SELECT 'published'::TEXT, p_package_id, p_version_id;
|
|
13013
|
+
RETURN;
|
|
13014
|
+
END IF;
|
|
13015
|
+
EXIT retry;
|
|
13016
|
+
END LOOP;
|
|
13017
|
+
|
|
13018
|
+
v_is_owner := v_pkg.owner_provider IS NOT NULL
|
|
13019
|
+
AND v_pkg.owner_provider IS NOT DISTINCT FROM v_owner_provider
|
|
13020
|
+
AND v_pkg.owner_subject IS NOT DISTINCT FROM v_owner_subject;
|
|
13021
|
+
IF NOT p_is_admin AND NOT v_is_owner
|
|
13022
|
+
AND NOT ${s}.cms_agent_package_is_editor(v_pkg.package_id, v_owner_provider, v_owner_subject) THEN
|
|
13023
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator, an editor, or an admin can publish new versions of "%"', p_name;
|
|
13024
|
+
END IF;
|
|
13025
|
+
|
|
13026
|
+
SELECT * INTO v_ver FROM ${s}.agent_package_versions v
|
|
13027
|
+
WHERE v.package_id = v_pkg.package_id AND v.semver = p_semver;
|
|
13028
|
+
IF FOUND THEN
|
|
13029
|
+
IF v_ver.sha256 = p_sha256 THEN
|
|
13030
|
+
RETURN QUERY SELECT 'noop'::TEXT, v_pkg.package_id, v_ver.version_id;
|
|
13031
|
+
RETURN;
|
|
13032
|
+
END IF;
|
|
13033
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_SEMVER_CONFLICT: %@% is already published with different content — published versions are immutable, bump the version', p_name, p_semver;
|
|
13034
|
+
END IF;
|
|
13035
|
+
|
|
13036
|
+
INSERT INTO ${s}.agent_package_versions
|
|
13037
|
+
(version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
|
|
13038
|
+
VALUES (p_version_id, v_pkg.package_id, p_semver, p_sha256, p_size_bytes,
|
|
13039
|
+
p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
|
|
13040
|
+
UPDATE ${s}.agent_packages
|
|
13041
|
+
SET active_version_id = p_version_id,
|
|
13042
|
+
source_id = COALESCE(p_source_id, ${s}.agent_packages.source_id)
|
|
13043
|
+
WHERE ${s}.agent_packages.package_id = v_pkg.package_id;
|
|
13044
|
+
PERFORM ${s}.cms_agent_registry_bump();
|
|
13045
|
+
RETURN QUERY SELECT 'published'::TEXT, v_pkg.package_id, p_version_id;
|
|
13046
|
+
END;
|
|
13047
|
+
$$ LANGUAGE plpgsql;
|
|
13048
|
+
|
|
13049
|
+
-- ── Grant / revoke / list ────────────────────────────────────────
|
|
13050
|
+
--
|
|
13051
|
+
-- All three pin the SHARED copy: editors only exist there. Grant and revoke
|
|
13052
|
+
-- are owner-or-admin (authz without editor mode). No registry epoch bump:
|
|
13053
|
+
-- who may edit changes nothing a worker installs.
|
|
13054
|
+
-- The shared copy of a name, or a legible refusal. Resolving with the
|
|
13055
|
+
-- selector pinned to 'shared' returns nothing when only a private copy
|
|
13056
|
+
-- exists, and "does not exist" is the wrong answer to give the person who
|
|
13057
|
+
-- owns that copy. So: no shared copy AND the actor can see a copy → NOT_SHARED;
|
|
13058
|
+
-- no copy the actor can see → NOT_FOUND.
|
|
13059
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_agent_package_require_shared(
|
|
13060
|
+
p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
|
|
13061
|
+
) RETURNS VOID AS $$
|
|
13062
|
+
BEGIN
|
|
13063
|
+
IF EXISTS (SELECT 1 FROM ${s}.agent_packages p WHERE p.name = p_name AND p.scope = 'shared') THEN
|
|
13064
|
+
RETURN;
|
|
13065
|
+
END IF;
|
|
13066
|
+
IF EXISTS (
|
|
13067
|
+
SELECT 1 FROM ${s}.agent_packages p
|
|
13068
|
+
WHERE p.name = p_name
|
|
13069
|
+
AND (p_is_admin
|
|
13070
|
+
OR (p.owner_provider = NULLIF(BTRIM(p_actor_provider), '')
|
|
13071
|
+
AND p.owner_subject = NULLIF(BTRIM(p_actor_subject), '')))
|
|
13072
|
+
) THEN
|
|
13073
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NOT_SHARED: "%" is not shared; editors exist only on a shared package — promote it first', p_name;
|
|
13074
|
+
END IF;
|
|
13075
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
|
|
13076
|
+
END;
|
|
13077
|
+
$$ LANGUAGE plpgsql;
|
|
13078
|
+
|
|
13079
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_grant_agent_package_editor(
|
|
13080
|
+
p_name TEXT, p_provider TEXT, p_subject TEXT,
|
|
13081
|
+
p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
|
|
13082
|
+
) RETURNS VOID AS $$
|
|
13083
|
+
DECLARE
|
|
13084
|
+
v_pkg ${s}.agent_packages;
|
|
13085
|
+
v_provider TEXT := NULLIF(BTRIM(p_provider), '');
|
|
13086
|
+
v_subject TEXT := NULLIF(BTRIM(p_subject), '');
|
|
13087
|
+
v_user_id BIGINT;
|
|
13088
|
+
v_granted_by BIGINT;
|
|
13089
|
+
BEGIN
|
|
13090
|
+
IF v_provider IS NULL OR v_subject IS NULL THEN
|
|
13091
|
+
RAISE EXCEPTION 'Grantee provider and subject are required';
|
|
13092
|
+
END IF;
|
|
13093
|
+
PERFORM ${s}.cms_agent_package_require_shared(p_name, p_actor_provider, p_actor_subject, p_is_admin);
|
|
13094
|
+
v_pkg := ${s}.cms_agent_package_authz(
|
|
13095
|
+
p_name, p_actor_provider, p_actor_subject, p_is_admin, 'shared', NULL, NULL);
|
|
13096
|
+
IF v_pkg.owner_provider = v_provider AND v_pkg.owner_subject = v_subject THEN
|
|
13097
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_EDITOR_IS_OWNER: the owner of "%" already has full access', p_name;
|
|
13098
|
+
END IF;
|
|
13099
|
+
|
|
13100
|
+
-- Create-only (0033): a grant never rewrites a sighted user's identity.
|
|
13101
|
+
INSERT INTO ${s}.users (provider, subject)
|
|
13102
|
+
VALUES (v_provider, v_subject)
|
|
13103
|
+
ON CONFLICT (provider, subject) DO NOTHING;
|
|
13104
|
+
SELECT u.user_id INTO v_user_id FROM ${s}.users u
|
|
13105
|
+
WHERE u.provider = v_provider AND u.subject = v_subject;
|
|
13106
|
+
|
|
13107
|
+
SELECT u.user_id INTO v_granted_by FROM ${s}.users u
|
|
13108
|
+
WHERE u.provider = NULLIF(BTRIM(p_actor_provider), '')
|
|
13109
|
+
AND u.subject = NULLIF(BTRIM(p_actor_subject), '');
|
|
13110
|
+
|
|
13111
|
+
INSERT INTO ${s}.agent_package_editors (package_id, user_id, granted_by)
|
|
13112
|
+
VALUES (v_pkg.package_id, v_user_id, v_granted_by)
|
|
13113
|
+
ON CONFLICT (package_id, user_id) DO NOTHING;
|
|
13114
|
+
END;
|
|
13115
|
+
$$ LANGUAGE plpgsql;
|
|
13116
|
+
|
|
13117
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_revoke_agent_package_editor(
|
|
13118
|
+
p_name TEXT, p_provider TEXT, p_subject TEXT,
|
|
13119
|
+
p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
|
|
13120
|
+
) RETURNS VOID AS $$
|
|
13121
|
+
DECLARE
|
|
13122
|
+
v_pkg ${s}.agent_packages;
|
|
13123
|
+
BEGIN
|
|
13124
|
+
PERFORM ${s}.cms_agent_package_require_shared(p_name, p_actor_provider, p_actor_subject, p_is_admin);
|
|
13125
|
+
v_pkg := ${s}.cms_agent_package_authz(
|
|
13126
|
+
p_name, p_actor_provider, p_actor_subject, p_is_admin, 'shared', NULL, NULL);
|
|
13127
|
+
DELETE FROM ${s}.agent_package_editors e
|
|
13128
|
+
USING ${s}.users u
|
|
13129
|
+
WHERE e.package_id = v_pkg.package_id
|
|
13130
|
+
AND u.user_id = e.user_id
|
|
13131
|
+
AND u.provider = NULLIF(BTRIM(p_provider), '')
|
|
13132
|
+
AND u.subject = NULLIF(BTRIM(p_subject), '');
|
|
13133
|
+
END;
|
|
13134
|
+
$$ LANGUAGE plpgsql;
|
|
13135
|
+
|
|
13136
|
+
-- Anyone who can see the shared package can see its editors: this is a
|
|
13137
|
+
-- trusted system, and who may edit a shared agent is not a secret.
|
|
13138
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_package_editors(
|
|
13139
|
+
p_name TEXT
|
|
13140
|
+
) RETURNS TABLE(
|
|
13141
|
+
provider TEXT, subject TEXT, email TEXT, display_name TEXT,
|
|
13142
|
+
granted_at TIMESTAMPTZ, granted_by_display TEXT
|
|
13143
|
+
) AS $$
|
|
13144
|
+
BEGIN
|
|
13145
|
+
-- "No editors" and "no such shared package" must not look the same.
|
|
13146
|
+
IF NOT EXISTS (SELECT 1 FROM ${s}.agent_packages p WHERE p.name = p_name AND p.scope = 'shared') THEN
|
|
13147
|
+
RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: there is no shared package named "%"', p_name;
|
|
13148
|
+
END IF;
|
|
13149
|
+
RETURN QUERY
|
|
13150
|
+
SELECT u.provider, u.subject, u.email, u.display_name,
|
|
13151
|
+
e.granted_at,
|
|
13152
|
+
COALESCE(g.display_name, g.email, g.subject) AS granted_by_display
|
|
13153
|
+
FROM ${s}.agent_packages p
|
|
13154
|
+
JOIN ${s}.agent_package_editors e ON e.package_id = p.package_id
|
|
13155
|
+
JOIN ${s}.users u ON u.user_id = e.user_id
|
|
13156
|
+
LEFT JOIN ${s}.users g ON g.user_id = e.granted_by
|
|
13157
|
+
WHERE p.name = p_name AND p.scope = 'shared'
|
|
13158
|
+
ORDER BY e.granted_at, u.user_id;
|
|
13159
|
+
END;
|
|
13160
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
13161
|
+
|
|
13162
|
+
-- ── Reads carry can_edit ─────────────────────────────────────────
|
|
13163
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_agent_packages(TEXT, TEXT, BOOLEAN);
|
|
13164
|
+
DROP FUNCTION IF EXISTS ${s}.cms_get_agent_package(TEXT, TEXT, TEXT, BOOLEAN, TEXT, TEXT, TEXT);
|
|
13165
|
+
|
|
13166
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_packages(
|
|
13167
|
+
p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
|
|
13168
|
+
) RETURNS TABLE(
|
|
13169
|
+
package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
|
|
13170
|
+
owner_provider TEXT, owner_subject TEXT,
|
|
13171
|
+
owner_email TEXT, owner_display_name TEXT,
|
|
13172
|
+
enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
|
|
13173
|
+
active_version_id TEXT, semver TEXT, sha256 TEXT, size_bytes BIGINT,
|
|
13174
|
+
artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
|
|
13175
|
+
version_created_at TIMESTAMPTZ, version_created_by TEXT,
|
|
13176
|
+
shadowed BOOLEAN, can_edit BOOLEAN
|
|
13177
|
+
) AS $$
|
|
13178
|
+
SELECT p.package_id, p.source_id, p.name, p.scope,
|
|
13179
|
+
p.owner_provider, p.owner_subject,
|
|
13180
|
+
u.email, u.display_name,
|
|
13181
|
+
p.enabled, p.created_by, p.created_at,
|
|
13182
|
+
v.version_id, v.semver, v.sha256, v.size_bytes,
|
|
13183
|
+
v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by,
|
|
13184
|
+
(p.scope = 'shared' AND EXISTS (
|
|
13185
|
+
SELECT 1 FROM ${s}.agent_packages o
|
|
13186
|
+
WHERE o.name = p.name AND o.scope = 'user' AND o.enabled
|
|
13187
|
+
AND o.owner_provider = BTRIM(p_viewer_provider)
|
|
13188
|
+
AND o.owner_subject = BTRIM(p_viewer_subject)
|
|
13189
|
+
)) AS shadowed,
|
|
13190
|
+
(p_is_admin
|
|
13191
|
+
OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
|
|
13192
|
+
OR ${s}.cms_agent_package_is_editor(p.package_id, p_viewer_provider, p_viewer_subject)
|
|
13193
|
+
) AS can_edit
|
|
13194
|
+
FROM ${s}.agent_packages p
|
|
13195
|
+
LEFT JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
|
|
13196
|
+
LEFT JOIN ${s}.users u
|
|
13197
|
+
ON u.provider = p.owner_provider AND u.subject = p.owner_subject
|
|
13198
|
+
WHERE p.scope = 'shared'
|
|
13199
|
+
OR p_is_admin
|
|
13200
|
+
OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
|
|
13201
|
+
ORDER BY p.name, p.scope, p.owner_provider, p.owner_subject;
|
|
13202
|
+
$$ LANGUAGE sql;
|
|
13203
|
+
|
|
13204
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_package(
|
|
13205
|
+
p_name TEXT, p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN,
|
|
13206
|
+
p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
|
|
13207
|
+
) RETURNS TABLE(
|
|
13208
|
+
package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
|
|
13209
|
+
owner_provider TEXT, owner_subject TEXT,
|
|
13210
|
+
owner_email TEXT, owner_display_name TEXT,
|
|
13211
|
+
enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
|
|
13212
|
+
active_version_id TEXT, version_id TEXT, semver TEXT, sha256 TEXT,
|
|
13213
|
+
size_bytes BIGINT, artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
|
|
13214
|
+
version_created_at TIMESTAMPTZ, version_created_by TEXT,
|
|
13215
|
+
can_edit BOOLEAN
|
|
13216
|
+
) AS $$
|
|
13217
|
+
DECLARE
|
|
13218
|
+
v_id TEXT;
|
|
13219
|
+
v_count INT;
|
|
13220
|
+
BEGIN
|
|
13221
|
+
v_id := ${s}.cms_resolve_agent_package_id(
|
|
13222
|
+
p_name, p_viewer_provider, p_viewer_subject,
|
|
13223
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, TRUE);
|
|
13224
|
+
IF v_id IS NULL THEN
|
|
13225
|
+
v_id := ${s}.cms_resolve_agent_package_id(
|
|
13226
|
+
p_name, p_viewer_provider, p_viewer_subject,
|
|
13227
|
+
p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, FALSE);
|
|
13228
|
+
END IF;
|
|
13229
|
+
|
|
13230
|
+
IF v_id IS NULL AND p_is_admin AND p_sel_scope IS NULL THEN
|
|
13231
|
+
SELECT count(*) INTO v_count FROM ${s}.agent_packages p WHERE p.name = p_name;
|
|
13232
|
+
IF v_count = 1 THEN
|
|
13233
|
+
SELECT p.package_id INTO v_id FROM ${s}.agent_packages p WHERE p.name = p_name;
|
|
13234
|
+
END IF;
|
|
13235
|
+
END IF;
|
|
13236
|
+
|
|
13237
|
+
IF v_id IS NULL THEN
|
|
13238
|
+
RETURN;
|
|
13239
|
+
END IF;
|
|
13240
|
+
|
|
13241
|
+
RETURN QUERY
|
|
13242
|
+
SELECT p.package_id, p.source_id, p.name, p.scope,
|
|
13243
|
+
p.owner_provider, p.owner_subject,
|
|
13244
|
+
u.email, u.display_name,
|
|
13245
|
+
p.enabled, p.created_by, p.created_at, p.active_version_id,
|
|
13246
|
+
v.version_id, v.semver, v.sha256, v.size_bytes,
|
|
13247
|
+
v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by,
|
|
13248
|
+
(p_is_admin
|
|
13249
|
+
OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
|
|
13250
|
+
OR ${s}.cms_agent_package_is_editor(p.package_id, p_viewer_provider, p_viewer_subject)
|
|
13251
|
+
) AS can_edit
|
|
13252
|
+
FROM ${s}.agent_packages p
|
|
13253
|
+
LEFT JOIN ${s}.agent_package_versions v ON v.package_id = p.package_id
|
|
13254
|
+
LEFT JOIN ${s}.users u
|
|
13255
|
+
ON u.provider = p.owner_provider AND u.subject = p.owner_subject
|
|
13256
|
+
WHERE p.package_id = v_id
|
|
13257
|
+
AND (p.scope = 'shared' OR p_is_admin
|
|
13258
|
+
OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject)))
|
|
13259
|
+
ORDER BY v.created_at DESC;
|
|
13260
|
+
END;
|
|
13261
|
+
$$ LANGUAGE plpgsql;
|
|
13262
|
+
|
|
13263
|
+
-- ── The picker: a name OR an email makes a user findable ─────────
|
|
13264
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_list_users(
|
|
13265
|
+
p_limit INT
|
|
13266
|
+
) RETURNS TABLE (
|
|
13267
|
+
provider TEXT,
|
|
13268
|
+
subject TEXT,
|
|
13269
|
+
email TEXT,
|
|
13270
|
+
display_name TEXT
|
|
13271
|
+
) AS $$
|
|
13272
|
+
DECLARE
|
|
13273
|
+
v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 500), 2000));
|
|
13274
|
+
BEGIN
|
|
13275
|
+
RETURN QUERY
|
|
13276
|
+
SELECT u.provider, u.subject, u.email, u.display_name
|
|
13277
|
+
FROM ${s}.users u
|
|
13278
|
+
WHERE NOT (u.provider = 'system' AND u.subject = 'system')
|
|
13279
|
+
AND NOT (u.provider = 'local' AND u.subject = 'default')
|
|
13280
|
+
-- The no-auth provider's anonymous principal is not a person either.
|
|
13281
|
+
AND NOT (u.provider = 'none')
|
|
13282
|
+
-- Sighted members carry a name or an email (0042 writes both from the
|
|
13283
|
+
-- token when present). A row with neither is a raw-id grant
|
|
13284
|
+
-- placeholder and stays hidden until that person signs in.
|
|
13285
|
+
AND (u.display_name IS NOT NULL OR u.email IS NOT NULL)
|
|
13286
|
+
ORDER BY u.updated_at DESC, u.user_id DESC
|
|
13287
|
+
LIMIT v_limit;
|
|
13288
|
+
END;
|
|
13289
|
+
$$ LANGUAGE plpgsql;
|
|
13290
|
+
|
|
13291
|
+
-- ── Ghost adoption must carry editor grants across ───────────────
|
|
13292
|
+
--
|
|
13293
|
+
-- 0032's cms_register_user adopts email-keyed placeholder rows on a real
|
|
13294
|
+
-- user's first sighting: shares move to the real user and the placeholder
|
|
13295
|
+
-- is deleted. agent_package_editors also references users(user_id), so a
|
|
13296
|
+
-- placeholder that was granted an editor row blocked that DELETE with an FK
|
|
13297
|
+
-- violation — and the whole registration rolled back, which meant the
|
|
13298
|
+
-- person could not sign in. Same body as 0032 plus the editor re-pointing.
|
|
13299
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_register_user(
|
|
13300
|
+
p_provider TEXT,
|
|
13301
|
+
p_subject TEXT,
|
|
13302
|
+
p_email TEXT,
|
|
13303
|
+
p_display_name TEXT
|
|
13304
|
+
) RETURNS BIGINT AS $$
|
|
13305
|
+
DECLARE
|
|
13306
|
+
v_provider TEXT := NULLIF(BTRIM(p_provider), '');
|
|
13307
|
+
v_subject TEXT := NULLIF(BTRIM(p_subject), '');
|
|
13308
|
+
v_email TEXT := NULLIF(BTRIM(p_email), '');
|
|
13309
|
+
v_display TEXT := NULLIF(BTRIM(p_display_name), '');
|
|
13310
|
+
v_user_id BIGINT;
|
|
13311
|
+
v_ghost BIGINT;
|
|
13312
|
+
BEGIN
|
|
13313
|
+
IF v_provider IS NULL OR v_subject IS NULL THEN
|
|
13314
|
+
RAISE EXCEPTION 'User provider and subject are required';
|
|
13315
|
+
END IF;
|
|
13316
|
+
|
|
13317
|
+
INSERT INTO ${s}.users (provider, subject, email, display_name)
|
|
13318
|
+
VALUES (v_provider, v_subject, v_email, v_display)
|
|
13319
|
+
ON CONFLICT (provider, subject) DO UPDATE
|
|
13320
|
+
SET email = COALESCE(EXCLUDED.email, ${s}.users.email),
|
|
13321
|
+
display_name = COALESCE(EXCLUDED.display_name, ${s}.users.display_name),
|
|
13322
|
+
updated_at = now()
|
|
13323
|
+
WHERE COALESCE(EXCLUDED.email, ${s}.users.email) IS DISTINCT FROM ${s}.users.email
|
|
13324
|
+
OR COALESCE(EXCLUDED.display_name, ${s}.users.display_name) IS DISTINCT FROM ${s}.users.display_name;
|
|
13325
|
+
|
|
13326
|
+
SELECT user_id INTO v_user_id
|
|
13327
|
+
FROM ${s}.users
|
|
13328
|
+
WHERE provider = v_provider AND subject = v_subject;
|
|
13329
|
+
|
|
13330
|
+
IF v_email IS NOT NULL THEN
|
|
13331
|
+
FOR v_ghost IN
|
|
13332
|
+
SELECT u.user_id FROM ${s}.users u
|
|
13333
|
+
WHERE u.provider = v_provider
|
|
13334
|
+
AND LOWER(u.subject) = LOWER(v_email)
|
|
13335
|
+
AND u.user_id <> v_user_id
|
|
13336
|
+
LOOP
|
|
13337
|
+
UPDATE ${s}.session_shares ss SET user_id = v_user_id
|
|
13338
|
+
WHERE ss.user_id = v_ghost
|
|
13339
|
+
AND NOT EXISTS (
|
|
13340
|
+
SELECT 1 FROM ${s}.session_shares e
|
|
13341
|
+
WHERE e.session_id = ss.session_id AND e.user_id = v_user_id
|
|
13342
|
+
);
|
|
13343
|
+
UPDATE ${s}.session_shares e SET access = 'write'
|
|
13344
|
+
FROM ${s}.session_shares g
|
|
13345
|
+
WHERE g.user_id = v_ghost AND g.session_id = e.session_id
|
|
13346
|
+
AND e.user_id = v_user_id AND g.access = 'write' AND e.access <> 'write';
|
|
13347
|
+
DELETE FROM ${s}.session_shares WHERE user_id = v_ghost;
|
|
13348
|
+
UPDATE ${s}.session_shares SET granted_by = v_user_id WHERE granted_by = v_ghost;
|
|
13349
|
+
-- Editor grants: move the ones the real user does not already
|
|
13350
|
+
-- hold, drop the duplicates, re-point granted_by.
|
|
13351
|
+
UPDATE ${s}.agent_package_editors ge SET user_id = v_user_id
|
|
13352
|
+
WHERE ge.user_id = v_ghost
|
|
13353
|
+
AND NOT EXISTS (
|
|
13354
|
+
SELECT 1 FROM ${s}.agent_package_editors e
|
|
13355
|
+
WHERE e.package_id = ge.package_id AND e.user_id = v_user_id
|
|
13356
|
+
);
|
|
13357
|
+
DELETE FROM ${s}.agent_package_editors WHERE user_id = v_ghost;
|
|
13358
|
+
UPDATE ${s}.agent_package_editors SET granted_by = v_user_id WHERE granted_by = v_ghost;
|
|
13359
|
+
UPDATE ${s}.session_owners SET user_id = v_user_id WHERE user_id = v_ghost;
|
|
13360
|
+
UPDATE ${s}.session_group_owners SET user_id = v_user_id WHERE user_id = v_ghost;
|
|
13361
|
+
DELETE FROM ${s}.users WHERE user_id = v_ghost;
|
|
13362
|
+
END LOOP;
|
|
13363
|
+
END IF;
|
|
13364
|
+
|
|
13365
|
+
RETURN v_user_id;
|
|
13366
|
+
END;
|
|
13367
|
+
$$ LANGUAGE plpgsql;
|
|
13368
|
+
`;
|
|
13369
|
+
}
|
|
13370
|
+
// ─── Migration 0064: the canvas KV store ─────────────────────────────
|
|
13371
|
+
//
|
|
13372
|
+
// Shared, durable, per-key state for interactive canvas apps
|
|
13373
|
+
// (docs/proposals/interactive-canvas-apps.md Part C). One row per
|
|
13374
|
+
// (session, slot, key); the value is the envelope {v, by, at}; `rev` is the
|
|
13375
|
+
// compare-and-swap token; deletes are tombstones (rev still advances, so a
|
|
13376
|
+
// live viewer drops the key and a late joiner never sees it).
|
|
13377
|
+
//
|
|
13378
|
+
// The rows live HERE, in the CMS, not in the facts table the proposal first
|
|
13379
|
+
// named: the facts store can be a different database (HorizonDB), and the
|
|
13380
|
+
// write must be atomic with its NOTIFY on the CMS connection. One statement
|
|
13381
|
+
// does the CAS check, the quota check, the upsert and the notify, so two
|
|
13382
|
+
// writers on one key serialize on the row and two viewers never see a
|
|
13383
|
+
// write without its ping.
|
|
13384
|
+
//
|
|
13385
|
+
// session_canvases gains the per-canvas access policy (Part D: who may
|
|
13386
|
+
// write) and the manifest's `kv` block cached at draw time (the app's half
|
|
13387
|
+
// of the switch), so a write never has to re-read the document.
|
|
13388
|
+
function migration_0064_canvas_kv(schema) {
|
|
13389
|
+
const s = `"${schema}"`;
|
|
13390
|
+
return `
|
|
13391
|
+
ALTER TABLE ${s}.session_canvases
|
|
13392
|
+
ADD COLUMN IF NOT EXISTS kv_access TEXT NOT NULL DEFAULT 'owner'
|
|
13393
|
+
CHECK (kv_access IN ('owner', 'readers', 'link'));
|
|
13394
|
+
ALTER TABLE ${s}.session_canvases
|
|
13395
|
+
ADD COLUMN IF NOT EXISTS kv_manifest JSONB;
|
|
13396
|
+
|
|
13397
|
+
CREATE TABLE IF NOT EXISTS ${s}.canvas_kv (
|
|
13398
|
+
session_id TEXT NOT NULL REFERENCES ${s}.sessions(session_id) ON DELETE CASCADE,
|
|
13399
|
+
slot SMALLINT NOT NULL CHECK (slot BETWEEN 1 AND 5),
|
|
13400
|
+
key TEXT NOT NULL,
|
|
13401
|
+
value JSONB NOT NULL,
|
|
13402
|
+
rev BIGINT NOT NULL DEFAULT 1,
|
|
13403
|
+
deleted_at TIMESTAMPTZ,
|
|
13404
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
13405
|
+
PRIMARY KEY (session_id, slot, key)
|
|
13406
|
+
);
|
|
13407
|
+
|
|
13408
|
+
-- Policy + manifest switch, upserting the 0045 row when the canvas has
|
|
13409
|
+
-- never been drawn (an owner may set the policy before the first draw).
|
|
13410
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_set_canvas_kv_access(
|
|
13411
|
+
p_session_id TEXT, p_slot INT, p_access TEXT
|
|
13412
|
+
) RETURNS VOID AS $$
|
|
13413
|
+
BEGIN
|
|
13414
|
+
IF p_access NOT IN ('owner', 'readers', 'link') THEN
|
|
13415
|
+
RAISE EXCEPTION 'CANVAS_KV_BAD_ACCESS: kv-access must be owner, readers or link, got "%"', p_access;
|
|
13416
|
+
END IF;
|
|
13417
|
+
INSERT INTO ${s}.session_canvases (session_id, slot, name, latest_rev, size_bytes, kv_access, updated_at)
|
|
13418
|
+
VALUES (p_session_id, p_slot, '', 0, NULL, p_access, now())
|
|
13419
|
+
ON CONFLICT (session_id, slot) DO UPDATE SET kv_access = EXCLUDED.kv_access, updated_at = now();
|
|
13420
|
+
END;
|
|
13421
|
+
$$ LANGUAGE plpgsql;
|
|
13422
|
+
|
|
13423
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_set_canvas_kv_manifest(
|
|
13424
|
+
p_session_id TEXT, p_slot INT, p_manifest JSONB
|
|
13425
|
+
) RETURNS VOID AS $$
|
|
13426
|
+
BEGIN
|
|
13427
|
+
INSERT INTO ${s}.session_canvases (session_id, slot, name, latest_rev, size_bytes, kv_manifest, updated_at)
|
|
13428
|
+
VALUES (p_session_id, p_slot, '', 0, NULL, p_manifest, now())
|
|
13429
|
+
ON CONFLICT (session_id, slot) DO UPDATE SET kv_manifest = EXCLUDED.kv_manifest, updated_at = now();
|
|
13430
|
+
END;
|
|
13431
|
+
$$ LANGUAGE plpgsql;
|
|
13432
|
+
|
|
13433
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_get_canvas_kv_settings(
|
|
13434
|
+
p_session_id TEXT, p_slot INT
|
|
13435
|
+
) RETURNS TABLE(kv_access TEXT, kv_manifest JSONB, latest_rev INTEGER) AS $$
|
|
13436
|
+
SELECT c.kv_access, c.kv_manifest, c.latest_rev
|
|
13437
|
+
FROM ${s}.session_canvases c
|
|
13438
|
+
WHERE c.session_id = p_session_id AND c.slot = p_slot;
|
|
13439
|
+
$$ LANGUAGE sql STABLE;
|
|
13440
|
+
|
|
13441
|
+
-- The write. p_value NULL = delete. p_if_match NULL = no CAS; 0 = the key
|
|
13442
|
+
-- must not be live (claim); N = the live rev must be N. Statuses:
|
|
13443
|
+
-- written | deleted | not_found | conflict | too_large | quota_keys | quota_bytes
|
|
13444
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_canvas_kv_write(
|
|
13445
|
+
p_session_id TEXT, p_slot INT, p_key TEXT, p_value JSONB, p_if_match BIGINT,
|
|
13446
|
+
p_max_keys INT, p_max_bytes BIGINT, p_max_value_bytes INT, p_schema TEXT
|
|
13447
|
+
) RETURNS TABLE(status TEXT, rev BIGINT, size_bytes INT) AS $$
|
|
13448
|
+
DECLARE
|
|
13449
|
+
v_rev BIGINT;
|
|
13450
|
+
v_deleted TIMESTAMPTZ;
|
|
13451
|
+
v_found BOOLEAN;
|
|
13452
|
+
v_live BOOLEAN;
|
|
13453
|
+
v_size INT;
|
|
13454
|
+
v_keys BIGINT;
|
|
13455
|
+
v_bytes BIGINT;
|
|
13456
|
+
v_new_rev BIGINT;
|
|
13457
|
+
BEGIN
|
|
13458
|
+
SELECT k.rev, k.deleted_at INTO v_rev, v_deleted
|
|
13459
|
+
FROM ${s}.canvas_kv k
|
|
13460
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.key = p_key
|
|
13461
|
+
FOR UPDATE;
|
|
13462
|
+
v_found := FOUND;
|
|
13463
|
+
v_live := v_found AND v_deleted IS NULL;
|
|
13464
|
+
|
|
13465
|
+
IF p_if_match IS NOT NULL THEN
|
|
13466
|
+
IF (p_if_match = 0 AND v_live) OR (p_if_match <> 0 AND (NOT v_live OR v_rev <> p_if_match)) THEN
|
|
13467
|
+
RETURN QUERY SELECT 'conflict'::TEXT, COALESCE(v_rev, 0)::BIGINT, NULL::INT;
|
|
13468
|
+
RETURN;
|
|
13469
|
+
END IF;
|
|
13470
|
+
END IF;
|
|
13471
|
+
|
|
13472
|
+
IF p_value IS NULL THEN
|
|
13473
|
+
IF NOT v_live THEN
|
|
13474
|
+
RETURN QUERY SELECT 'not_found'::TEXT, COALESCE(v_rev, 0)::BIGINT, NULL::INT;
|
|
13475
|
+
RETURN;
|
|
13476
|
+
END IF;
|
|
13477
|
+
UPDATE ${s}.canvas_kv k SET deleted_at = now(), rev = k.rev + 1, updated_at = now()
|
|
13478
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.key = p_key
|
|
13479
|
+
RETURNING k.rev INTO v_new_rev;
|
|
13480
|
+
PERFORM pg_notify('pilotswarm_canvas_live', json_build_object(
|
|
13481
|
+
'schema', p_schema, 'sessionId', p_session_id, 'slot', p_slot,
|
|
13482
|
+
'kind', 'kv', 'key', p_key, 'rev', v_new_rev, 'op', 'delete')::text);
|
|
13483
|
+
RETURN QUERY SELECT 'deleted'::TEXT, v_new_rev, 0;
|
|
13484
|
+
RETURN;
|
|
13485
|
+
END IF;
|
|
13486
|
+
|
|
13487
|
+
v_size := octet_length(p_value::text);
|
|
13488
|
+
IF v_size > p_max_value_bytes THEN
|
|
13489
|
+
RETURN QUERY SELECT 'too_large'::TEXT, COALESCE(v_rev, 0)::BIGINT, v_size;
|
|
13490
|
+
RETURN;
|
|
13491
|
+
END IF;
|
|
13492
|
+
|
|
13493
|
+
-- Budget check against every OTHER live key of this canvas.
|
|
13494
|
+
SELECT count(*), COALESCE(sum(octet_length(k.value::text)), 0) INTO v_keys, v_bytes
|
|
13495
|
+
FROM ${s}.canvas_kv k
|
|
13496
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.deleted_at IS NULL AND k.key <> p_key;
|
|
13497
|
+
IF NOT v_live AND v_keys + 1 > p_max_keys THEN
|
|
13498
|
+
RETURN QUERY SELECT 'quota_keys'::TEXT, COALESCE(v_rev, 0)::BIGINT, v_size;
|
|
13499
|
+
RETURN;
|
|
13500
|
+
END IF;
|
|
13501
|
+
IF v_bytes + v_size > p_max_bytes THEN
|
|
13502
|
+
RETURN QUERY SELECT 'quota_bytes'::TEXT, COALESCE(v_rev, 0)::BIGINT, v_size;
|
|
13503
|
+
RETURN;
|
|
13504
|
+
END IF;
|
|
13505
|
+
|
|
13506
|
+
-- A claim (if_match = 0) must not become an update under a concurrent
|
|
13507
|
+
-- first insert: the upsert only resurrects a tombstone, never overwrites
|
|
13508
|
+
-- a live row, and a missing RETURNING row is reported as a conflict.
|
|
13509
|
+
IF p_if_match = 0 THEN
|
|
13510
|
+
INSERT INTO ${s}.canvas_kv (session_id, slot, key, value, rev, deleted_at, updated_at)
|
|
13511
|
+
VALUES (p_session_id, p_slot, p_key, p_value, 1, NULL, now())
|
|
13512
|
+
ON CONFLICT (session_id, slot, key) DO UPDATE SET
|
|
13513
|
+
value = EXCLUDED.value, rev = ${s}.canvas_kv.rev + 1, deleted_at = NULL, updated_at = now()
|
|
13514
|
+
WHERE ${s}.canvas_kv.deleted_at IS NOT NULL
|
|
13515
|
+
RETURNING ${s}.canvas_kv.rev INTO v_new_rev;
|
|
13516
|
+
IF v_new_rev IS NULL THEN
|
|
13517
|
+
RETURN QUERY SELECT 'conflict'::TEXT, COALESCE(v_rev, 0)::BIGINT, v_size;
|
|
13518
|
+
RETURN;
|
|
13519
|
+
END IF;
|
|
13520
|
+
ELSE
|
|
13521
|
+
INSERT INTO ${s}.canvas_kv (session_id, slot, key, value, rev, deleted_at, updated_at)
|
|
13522
|
+
VALUES (p_session_id, p_slot, p_key, p_value, 1, NULL, now())
|
|
13523
|
+
ON CONFLICT (session_id, slot, key) DO UPDATE SET
|
|
13524
|
+
value = EXCLUDED.value, rev = ${s}.canvas_kv.rev + 1, deleted_at = NULL, updated_at = now()
|
|
13525
|
+
RETURNING ${s}.canvas_kv.rev INTO v_new_rev;
|
|
13526
|
+
END IF;
|
|
13527
|
+
|
|
13528
|
+
-- The value rides the ping when the envelope is small; a bigger value
|
|
13529
|
+
-- ships a pointer and the viewer fetches that one key. pg_notify hard
|
|
13530
|
+
-- errors past 8000 bytes and that would roll back the write.
|
|
13531
|
+
PERFORM pg_notify('pilotswarm_canvas_live', (
|
|
13532
|
+
SELECT CASE WHEN octet_length(m.with_value) <= 7000 THEN m.with_value ELSE m.pointer END
|
|
13533
|
+
FROM (SELECT
|
|
13534
|
+
json_build_object('schema', p_schema, 'sessionId', p_session_id, 'slot', p_slot,
|
|
13535
|
+
'kind', 'kv', 'key', p_key, 'rev', v_new_rev, 'op', 'put', 'value', p_value)::text AS with_value,
|
|
13536
|
+
json_build_object('schema', p_schema, 'sessionId', p_session_id, 'slot', p_slot,
|
|
13537
|
+
'kind', 'kv', 'key', p_key, 'rev', v_new_rev, 'op', 'put')::text AS pointer) m));
|
|
13538
|
+
RETURN QUERY SELECT 'written'::TEXT, v_new_rev, v_size;
|
|
13539
|
+
END;
|
|
13540
|
+
$$ LANGUAGE plpgsql;
|
|
13541
|
+
|
|
13542
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_canvas_kv_get(
|
|
13543
|
+
p_session_id TEXT, p_slot INT, p_key TEXT
|
|
13544
|
+
) RETURNS TABLE(key TEXT, value JSONB, rev BIGINT, updated_at TIMESTAMPTZ) AS $$
|
|
13545
|
+
SELECT k.key, k.value, k.rev, k.updated_at
|
|
13546
|
+
FROM ${s}.canvas_kv k
|
|
13547
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.key = p_key AND k.deleted_at IS NULL;
|
|
13548
|
+
$$ LANGUAGE sql STABLE;
|
|
13549
|
+
|
|
13550
|
+
-- Prefix listing, key-ordered, cursor = the last key of the previous page.
|
|
13551
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_canvas_kv_list(
|
|
13552
|
+
p_session_id TEXT, p_slot INT, p_prefix TEXT, p_limit INT, p_after_key TEXT
|
|
13553
|
+
) RETURNS TABLE(key TEXT, value JSONB, rev BIGINT, updated_at TIMESTAMPTZ) AS $$
|
|
13554
|
+
SELECT k.key, k.value, k.rev, k.updated_at
|
|
13555
|
+
FROM ${s}.canvas_kv k
|
|
13556
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.deleted_at IS NULL
|
|
13557
|
+
AND (p_prefix IS NULL OR p_prefix = '' OR left(k.key, length(p_prefix)) = p_prefix)
|
|
13558
|
+
AND (p_after_key IS NULL OR k.key > p_after_key)
|
|
13559
|
+
ORDER BY k.key
|
|
13560
|
+
LIMIT GREATEST(1, LEAST(COALESCE(p_limit, 200), 200));
|
|
13561
|
+
$$ LANGUAGE sql STABLE;
|
|
13562
|
+
|
|
13563
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_canvas_kv_stats(
|
|
13564
|
+
p_session_id TEXT, p_slot INT
|
|
13565
|
+
) RETURNS TABLE(keys BIGINT, bytes BIGINT) AS $$
|
|
13566
|
+
SELECT count(*)::BIGINT, COALESCE(sum(octet_length(k.value::text)), 0)::BIGINT
|
|
13567
|
+
FROM ${s}.canvas_kv k
|
|
13568
|
+
WHERE k.session_id = p_session_id AND k.slot = p_slot AND k.deleted_at IS NULL;
|
|
13569
|
+
$$ LANGUAGE sql STABLE;
|
|
13570
|
+
`;
|
|
13571
|
+
}
|
|
12750
13572
|
//# sourceMappingURL=cms-migrations.js.map
|