@team-semicolon/semicolony-cli 4.18.51 → 4.18.53
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/bundle.js +1524 -1046
- package/migrations/149_hive_cron_capability_templates.sql +44 -0
- package/migrations/150_hive_cron_action_contract.sql +38 -0
- package/migrations/151_hive_cron_occurrence_leases.sql +150 -0
- package/migrations/152_hive_cron_relay_outbox.sql +243 -0
- package/migrations/153_hive_cron_evidence_uniqueness.sql +9 -0
- package/migrations/154_hive_cron_evidence_provider_grants.sql +816 -0
- package/migrations/155_hive_cron_occurrence_lease_provider_grants.sql +44 -0
- package/migrations/156_hive_cron_relay_outbox_provider_grants.sql +47 -0
- package/migrations/157_hive_cron_trusted_config_revisions.sql +243 -0
- package/migrations/158_hive_cron_typed_dispatch_outbox.sql +1406 -0
- package/migrations/159_hive_cron_shadow_cursor.sql +600 -0
- package/migrations/160_hive_cron_failure_alert_outbox.sql +406 -0
- package/migrations/161_skill_governance.sql +529 -0
- package/migrations/161_workspace_projection_grants.sql +39 -0
- package/migrations/162_sc_app_kb_history_trigger_hardening.sql +96 -0
- package/migrations/163_workspace_projection_grants_sc_app_read.sql +21 -0
- package/package.json +4 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- 149_hive_cron_capability_templates.sql
|
|
2
|
+
-- SEMO Ops Hive cron parity: reviewed, non-executable capability templates.
|
|
3
|
+
--
|
|
4
|
+
-- This table stores policy template metadata only. It does not enqueue, grant, enable,
|
|
5
|
+
-- execute, or retire any cron job. The migration runner wraps this file in one
|
|
6
|
+
-- transaction, so do not add BEGIN/COMMIT here.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS semicolony.cron_capability_templates (
|
|
9
|
+
bot_id TEXT NOT NULL,
|
|
10
|
+
job_id TEXT NOT NULL,
|
|
11
|
+
runtime_source TEXT NOT NULL,
|
|
12
|
+
permission TEXT NOT NULL CHECK (permission IN ('read', 'make', 'operate')),
|
|
13
|
+
resource_scopes TEXT[] NOT NULL CHECK (cardinality(resource_scopes) > 0),
|
|
14
|
+
destination TEXT NOT NULL,
|
|
15
|
+
timeout_seconds INTEGER NOT NULL CHECK (timeout_seconds > 0 AND timeout_seconds <= 3600),
|
|
16
|
+
idempotency_key_template TEXT NOT NULL,
|
|
17
|
+
expected_outcome_digest TEXT NOT NULL CHECK (expected_outcome_digest ~ '^sha256:[a-f0-9]{64}$'),
|
|
18
|
+
source_hash TEXT NOT NULL CHECK (source_hash ~ '^sha256:[a-f0-9]{64}$'),
|
|
19
|
+
legacy_profile TEXT NOT NULL,
|
|
20
|
+
legacy_job_id TEXT NOT NULL,
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
22
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
23
|
+
PRIMARY KEY (bot_id, job_id, runtime_source),
|
|
24
|
+
CHECK (bot_id = 'semi'),
|
|
25
|
+
CHECK (runtime_source = 'hive-cron-v2'),
|
|
26
|
+
CHECK (legacy_job_id = job_id),
|
|
27
|
+
CHECK (idempotency_key_template = 'cron:' || job_id || ':{scheduled_for}'),
|
|
28
|
+
CHECK (NOT ('*' = ANY(resource_scopes))),
|
|
29
|
+
CHECK (array_position(resource_scopes, NULL) IS NULL),
|
|
30
|
+
FOREIGN KEY (bot_id, job_id)
|
|
31
|
+
REFERENCES semicolony.bot_cron_jobs(bot_id, job_id)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
COMMENT ON TABLE semicolony.cron_capability_templates IS
|
|
35
|
+
'Non-executable reviewed capability templates for disabled Hive cron parity imports.';
|
|
36
|
+
|
|
37
|
+
COMMENT ON COLUMN semicolony.cron_capability_templates.permission IS
|
|
38
|
+
'Reviewed maximum permission for a scheduled occurrence lease: read | make | operate.';
|
|
39
|
+
|
|
40
|
+
COMMENT ON COLUMN semicolony.cron_capability_templates.resource_scopes IS
|
|
41
|
+
'Explicit resource scopes used to mint a future per-occurrence lease. No wildcard scopes.';
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_cron_capability_templates_legacy_job
|
|
44
|
+
ON semicolony.cron_capability_templates(legacy_profile, legacy_job_id);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- 150_hive_cron_action_contract.sql
|
|
2
|
+
-- Adds a fail-closed typed action binding to reviewed Hive cron templates.
|
|
3
|
+
-- Existing templates remain unregistered because all three columns are nullable.
|
|
4
|
+
-- The migration runner supplies the transaction boundary; do not add one here.
|
|
5
|
+
|
|
6
|
+
ALTER TABLE semicolony.cron_capability_templates
|
|
7
|
+
ADD COLUMN IF NOT EXISTS action_kind TEXT,
|
|
8
|
+
ADD COLUMN IF NOT EXISTS action_version INTEGER,
|
|
9
|
+
ADD COLUMN IF NOT EXISTS action_contract_digest TEXT;
|
|
10
|
+
|
|
11
|
+
ALTER TABLE semicolony.cron_capability_templates
|
|
12
|
+
DROP CONSTRAINT IF EXISTS cron_capability_templates_action_contract_check;
|
|
13
|
+
|
|
14
|
+
ALTER TABLE semicolony.cron_capability_templates
|
|
15
|
+
ADD CONSTRAINT cron_capability_templates_action_contract_check CHECK (
|
|
16
|
+
(
|
|
17
|
+
action_kind IS NULL
|
|
18
|
+
AND action_version IS NULL
|
|
19
|
+
AND action_contract_digest IS NULL
|
|
20
|
+
)
|
|
21
|
+
OR
|
|
22
|
+
(
|
|
23
|
+
action_kind IS NOT NULL
|
|
24
|
+
AND action_version IS NOT NULL
|
|
25
|
+
AND action_contract_digest IS NOT NULL
|
|
26
|
+
AND action_version > 0
|
|
27
|
+
AND action_contract_digest ~ '^[a-f0-9]{64}$'
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
COMMENT ON COLUMN semicolony.cron_capability_templates.action_kind IS
|
|
32
|
+
'Registered typed action kind; NULL means the capability template is not executable.';
|
|
33
|
+
|
|
34
|
+
COMMENT ON COLUMN semicolony.cron_capability_templates.action_version IS
|
|
35
|
+
'Positive typed action contract version; NULL means unregistered.';
|
|
36
|
+
|
|
37
|
+
COMMENT ON COLUMN semicolony.cron_capability_templates.action_contract_digest IS
|
|
38
|
+
'Raw lowercase SHA-256 digest of ActionContractCanonicalV1; excludes Legacy SEMO source_hash provenance.';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
-- @schema-retarget: off
|
|
2
|
+
-- 151_hive_cron_occurrence_leases.sql
|
|
3
|
+
-- Durable ownership for Hive cron typed read-action occurrences.
|
|
4
|
+
-- The migration runner supplies the transaction boundary; do not add BEGIN/COMMIT.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS semicolony.cron_read_action_occurrence_leases (
|
|
7
|
+
idempotency_key TEXT PRIMARY KEY
|
|
8
|
+
CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
|
|
9
|
+
claim_id TEXT NOT NULL UNIQUE
|
|
10
|
+
CHECK (claim_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
|
|
11
|
+
state TEXT NOT NULL DEFAULT 'acquired'
|
|
12
|
+
CHECK (state IN ('acquired', 'completed', 'released_failed')),
|
|
13
|
+
acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
14
|
+
completed_at TIMESTAMPTZ,
|
|
15
|
+
CHECK (
|
|
16
|
+
(state = 'acquired' AND completed_at IS NULL)
|
|
17
|
+
OR (state IN ('completed', 'released_failed') AND completed_at IS NOT NULL)
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
COMMENT ON TABLE semicolony.cron_read_action_occurrence_leases IS
|
|
22
|
+
'Durable occurrence ownership and terminal tombstones for Hive cron typed read actions.';
|
|
23
|
+
|
|
24
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_protect_terminal_lease()
|
|
25
|
+
RETURNS trigger
|
|
26
|
+
LANGUAGE plpgsql
|
|
27
|
+
SECURITY DEFINER
|
|
28
|
+
SET search_path = pg_catalog
|
|
29
|
+
AS $$
|
|
30
|
+
BEGIN
|
|
31
|
+
IF OLD.state IN ('completed', 'released_failed') THEN
|
|
32
|
+
RAISE EXCEPTION 'terminal occurrence lease is immutable'
|
|
33
|
+
USING ERRCODE = '23514';
|
|
34
|
+
END IF;
|
|
35
|
+
IF TG_OP = 'DELETE' THEN
|
|
36
|
+
RETURN OLD;
|
|
37
|
+
END IF;
|
|
38
|
+
RETURN NEW;
|
|
39
|
+
END;
|
|
40
|
+
$$;
|
|
41
|
+
|
|
42
|
+
DROP TRIGGER IF EXISTS hive_cron_terminal_lease_guard
|
|
43
|
+
ON semicolony.cron_read_action_occurrence_leases;
|
|
44
|
+
CREATE TRIGGER hive_cron_terminal_lease_guard
|
|
45
|
+
BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_occurrence_leases
|
|
46
|
+
FOR EACH ROW
|
|
47
|
+
EXECUTE FUNCTION semicolony.hive_cron_protect_terminal_lease();
|
|
48
|
+
|
|
49
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_acquire_lease(
|
|
50
|
+
p_job_id text,
|
|
51
|
+
p_idempotency_key text,
|
|
52
|
+
p_claim_id text
|
|
53
|
+
)
|
|
54
|
+
RETURNS TABLE (outcome text)
|
|
55
|
+
LANGUAGE plpgsql
|
|
56
|
+
SECURITY DEFINER
|
|
57
|
+
SET search_path = pg_catalog
|
|
58
|
+
AS $$
|
|
59
|
+
DECLARE
|
|
60
|
+
inserted_count integer;
|
|
61
|
+
key_prefix text := 'cron:' || p_job_id || ':';
|
|
62
|
+
BEGIN
|
|
63
|
+
IF p_job_id IS NULL
|
|
64
|
+
OR p_idempotency_key IS NULL
|
|
65
|
+
OR p_claim_id IS NULL
|
|
66
|
+
OR pg_catalog.char_length(p_idempotency_key)
|
|
67
|
+
IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
|
|
68
|
+
OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
|
|
69
|
+
IS DISTINCT FROM key_prefix
|
|
70
|
+
OR pg_catalog.right(p_idempotency_key, 24)
|
|
71
|
+
!~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
72
|
+
OR NOT EXISTS (
|
|
73
|
+
SELECT 1
|
|
74
|
+
FROM semicolony.cron_capability_templates AS template
|
|
75
|
+
WHERE template.bot_id = 'semi'
|
|
76
|
+
AND template.job_id = p_job_id
|
|
77
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
78
|
+
AND template.action_kind IS NOT NULL
|
|
79
|
+
AND template.action_version IS NOT NULL
|
|
80
|
+
AND template.action_contract_digest IS NOT NULL
|
|
81
|
+
) THEN
|
|
82
|
+
RETURN;
|
|
83
|
+
END IF;
|
|
84
|
+
|
|
85
|
+
INSERT INTO semicolony.cron_read_action_occurrence_leases (
|
|
86
|
+
idempotency_key,
|
|
87
|
+
claim_id
|
|
88
|
+
)
|
|
89
|
+
VALUES (p_idempotency_key, p_claim_id)
|
|
90
|
+
ON CONFLICT (idempotency_key) DO NOTHING;
|
|
91
|
+
GET DIAGNOSTICS inserted_count = ROW_COUNT;
|
|
92
|
+
|
|
93
|
+
RETURN QUERY
|
|
94
|
+
SELECT CASE WHEN inserted_count = 1 THEN 'acquired'::text ELSE 'duplicate'::text END;
|
|
95
|
+
END;
|
|
96
|
+
$$;
|
|
97
|
+
|
|
98
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_release_failed(
|
|
99
|
+
p_job_id text,
|
|
100
|
+
p_idempotency_key text,
|
|
101
|
+
p_claim_id text
|
|
102
|
+
)
|
|
103
|
+
RETURNS TABLE (outcome text)
|
|
104
|
+
LANGUAGE plpgsql
|
|
105
|
+
SECURITY DEFINER
|
|
106
|
+
SET search_path = pg_catalog
|
|
107
|
+
AS $$
|
|
108
|
+
DECLARE
|
|
109
|
+
released_count integer;
|
|
110
|
+
key_prefix text := 'cron:' || p_job_id || ':';
|
|
111
|
+
BEGIN
|
|
112
|
+
IF p_job_id IS NULL
|
|
113
|
+
OR p_idempotency_key IS NULL
|
|
114
|
+
OR p_claim_id IS NULL
|
|
115
|
+
OR pg_catalog.char_length(p_idempotency_key)
|
|
116
|
+
IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
|
|
117
|
+
OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
|
|
118
|
+
IS DISTINCT FROM key_prefix
|
|
119
|
+
OR pg_catalog.right(p_idempotency_key, 24)
|
|
120
|
+
!~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
121
|
+
OR NOT EXISTS (
|
|
122
|
+
SELECT 1
|
|
123
|
+
FROM semicolony.cron_capability_templates AS template
|
|
124
|
+
WHERE template.bot_id = 'semi'
|
|
125
|
+
AND template.job_id = p_job_id
|
|
126
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
127
|
+
AND template.action_kind IS NOT NULL
|
|
128
|
+
AND template.action_version IS NOT NULL
|
|
129
|
+
AND template.action_contract_digest IS NOT NULL
|
|
130
|
+
) THEN
|
|
131
|
+
RETURN;
|
|
132
|
+
END IF;
|
|
133
|
+
|
|
134
|
+
UPDATE semicolony.cron_read_action_occurrence_leases
|
|
135
|
+
SET state = 'released_failed',
|
|
136
|
+
completed_at = pg_catalog.clock_timestamp()
|
|
137
|
+
WHERE idempotency_key = p_idempotency_key
|
|
138
|
+
AND claim_id = p_claim_id
|
|
139
|
+
AND state = 'acquired';
|
|
140
|
+
GET DIAGNOSTICS released_count = ROW_COUNT;
|
|
141
|
+
|
|
142
|
+
IF released_count = 1 THEN
|
|
143
|
+
RETURN QUERY SELECT 'released_failed'::text;
|
|
144
|
+
END IF;
|
|
145
|
+
END;
|
|
146
|
+
$$;
|
|
147
|
+
|
|
148
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_terminal_lease() FROM PUBLIC, sc_app;
|
|
149
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_acquire_lease(text, text, text) FROM PUBLIC, sc_app;
|
|
150
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_release_failed(text, text, text) FROM PUBLIC, sc_app;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
-- @schema-retarget: off
|
|
2
|
+
-- 152_hive_cron_relay_outbox.sql
|
|
3
|
+
-- Digest-only persistent relay reservation and terminal tombstones.
|
|
4
|
+
-- The migration runner supplies the transaction boundary; do not add BEGIN/COMMIT.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS semicolony.cron_read_action_relay_outbox (
|
|
7
|
+
idempotency_key TEXT PRIMARY KEY
|
|
8
|
+
CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
|
|
9
|
+
request_digest TEXT NOT NULL
|
|
10
|
+
CHECK (request_digest ~ '^[a-f0-9]{64}$'),
|
|
11
|
+
state TEXT NOT NULL DEFAULT 'pending'
|
|
12
|
+
CHECK (state IN ('pending', 'completed')),
|
|
13
|
+
outcome_status TEXT
|
|
14
|
+
CHECK (outcome_status IN ('delivered', 'suppressed', 'failed')),
|
|
15
|
+
error_code TEXT,
|
|
16
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
17
|
+
completed_at TIMESTAMPTZ,
|
|
18
|
+
CHECK (
|
|
19
|
+
(state = 'pending' AND outcome_status IS NULL AND error_code IS NULL AND completed_at IS NULL)
|
|
20
|
+
OR
|
|
21
|
+
(state = 'completed' AND outcome_status IN ('delivered', 'suppressed')
|
|
22
|
+
AND error_code IS NULL AND completed_at IS NOT NULL)
|
|
23
|
+
OR
|
|
24
|
+
(state = 'completed' AND outcome_status = 'failed'
|
|
25
|
+
AND error_code = 'delivery_failed' AND completed_at IS NOT NULL)
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_cron_read_action_relay_outbox_pending_age
|
|
30
|
+
ON semicolony.cron_read_action_relay_outbox (created_at)
|
|
31
|
+
WHERE state = 'pending';
|
|
32
|
+
|
|
33
|
+
COMMENT ON TABLE semicolony.cron_read_action_relay_outbox IS
|
|
34
|
+
'Digest-only relay reservation and terminal outcomes for Hive cron typed read actions.';
|
|
35
|
+
|
|
36
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_protect_terminal_outbox()
|
|
37
|
+
RETURNS trigger
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
SECURITY DEFINER
|
|
40
|
+
SET search_path = pg_catalog
|
|
41
|
+
AS $$
|
|
42
|
+
BEGIN
|
|
43
|
+
IF OLD.state = 'completed' THEN
|
|
44
|
+
RAISE EXCEPTION 'terminal relay outbox row is immutable'
|
|
45
|
+
USING ERRCODE = '23514';
|
|
46
|
+
END IF;
|
|
47
|
+
IF TG_OP = 'DELETE' THEN
|
|
48
|
+
RETURN OLD;
|
|
49
|
+
END IF;
|
|
50
|
+
RETURN NEW;
|
|
51
|
+
END;
|
|
52
|
+
$$;
|
|
53
|
+
|
|
54
|
+
DROP TRIGGER IF EXISTS hive_cron_terminal_outbox_guard
|
|
55
|
+
ON semicolony.cron_read_action_relay_outbox;
|
|
56
|
+
CREATE TRIGGER hive_cron_terminal_outbox_guard
|
|
57
|
+
BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_relay_outbox
|
|
58
|
+
FOR EACH ROW
|
|
59
|
+
EXECUTE FUNCTION semicolony.hive_cron_protect_terminal_outbox();
|
|
60
|
+
|
|
61
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_put_outbox(
|
|
62
|
+
p_job_id text,
|
|
63
|
+
p_idempotency_key text,
|
|
64
|
+
p_request_digest text
|
|
65
|
+
)
|
|
66
|
+
RETURNS TABLE (mutation_status text)
|
|
67
|
+
LANGUAGE plpgsql
|
|
68
|
+
SECURITY DEFINER
|
|
69
|
+
SET search_path = pg_catalog
|
|
70
|
+
AS $$
|
|
71
|
+
DECLARE
|
|
72
|
+
inserted_count integer;
|
|
73
|
+
key_prefix text := 'cron:' || p_job_id || ':';
|
|
74
|
+
BEGIN
|
|
75
|
+
IF p_job_id IS NULL
|
|
76
|
+
OR p_idempotency_key IS NULL
|
|
77
|
+
OR p_request_digest !~ '^[a-f0-9]{64}$'
|
|
78
|
+
OR pg_catalog.char_length(p_idempotency_key)
|
|
79
|
+
IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
|
|
80
|
+
OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
|
|
81
|
+
IS DISTINCT FROM key_prefix
|
|
82
|
+
OR pg_catalog.right(p_idempotency_key, 24)
|
|
83
|
+
!~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
84
|
+
OR NOT EXISTS (
|
|
85
|
+
SELECT 1
|
|
86
|
+
FROM semicolony.cron_capability_templates AS template
|
|
87
|
+
WHERE template.bot_id = 'semi'
|
|
88
|
+
AND template.job_id = p_job_id
|
|
89
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
90
|
+
AND template.action_kind IS NOT NULL
|
|
91
|
+
AND template.action_version IS NOT NULL
|
|
92
|
+
AND template.action_contract_digest IS NOT NULL
|
|
93
|
+
) THEN
|
|
94
|
+
RETURN;
|
|
95
|
+
END IF;
|
|
96
|
+
|
|
97
|
+
INSERT INTO semicolony.cron_read_action_relay_outbox AS outbox (
|
|
98
|
+
idempotency_key,
|
|
99
|
+
request_digest
|
|
100
|
+
)
|
|
101
|
+
VALUES (p_idempotency_key, p_request_digest)
|
|
102
|
+
ON CONFLICT (idempotency_key) DO NOTHING
|
|
103
|
+
;
|
|
104
|
+
GET DIAGNOSTICS inserted_count = ROW_COUNT;
|
|
105
|
+
|
|
106
|
+
RETURN QUERY
|
|
107
|
+
SELECT CASE WHEN inserted_count = 1 THEN 'inserted'::text ELSE 'existing'::text END;
|
|
108
|
+
END;
|
|
109
|
+
$$;
|
|
110
|
+
|
|
111
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_get_outbox(
|
|
112
|
+
p_job_id text,
|
|
113
|
+
p_idempotency_key text,
|
|
114
|
+
p_request_digest text
|
|
115
|
+
)
|
|
116
|
+
RETURNS TABLE (
|
|
117
|
+
lookup_status text,
|
|
118
|
+
outcome_status text,
|
|
119
|
+
error_code text
|
|
120
|
+
)
|
|
121
|
+
LANGUAGE plpgsql
|
|
122
|
+
SECURITY DEFINER
|
|
123
|
+
SET search_path = pg_catalog
|
|
124
|
+
AS $$
|
|
125
|
+
DECLARE
|
|
126
|
+
stored_digest text;
|
|
127
|
+
stored_state text;
|
|
128
|
+
stored_outcome text;
|
|
129
|
+
stored_error text;
|
|
130
|
+
key_prefix text := 'cron:' || p_job_id || ':';
|
|
131
|
+
BEGIN
|
|
132
|
+
IF p_job_id IS NULL
|
|
133
|
+
OR p_idempotency_key IS NULL
|
|
134
|
+
OR p_request_digest !~ '^[a-f0-9]{64}$'
|
|
135
|
+
OR pg_catalog.char_length(p_idempotency_key)
|
|
136
|
+
IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
|
|
137
|
+
OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
|
|
138
|
+
IS DISTINCT FROM key_prefix
|
|
139
|
+
OR pg_catalog.right(p_idempotency_key, 24)
|
|
140
|
+
!~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
141
|
+
OR NOT EXISTS (
|
|
142
|
+
SELECT 1
|
|
143
|
+
FROM semicolony.cron_capability_templates AS template
|
|
144
|
+
WHERE template.bot_id = 'semi'
|
|
145
|
+
AND template.job_id = p_job_id
|
|
146
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
147
|
+
AND template.action_kind IS NOT NULL
|
|
148
|
+
AND template.action_version IS NOT NULL
|
|
149
|
+
AND template.action_contract_digest IS NOT NULL
|
|
150
|
+
) THEN
|
|
151
|
+
RETURN;
|
|
152
|
+
END IF;
|
|
153
|
+
|
|
154
|
+
SELECT outbox.request_digest,
|
|
155
|
+
outbox.state,
|
|
156
|
+
outbox.outcome_status,
|
|
157
|
+
outbox.error_code
|
|
158
|
+
INTO stored_digest, stored_state, stored_outcome, stored_error
|
|
159
|
+
FROM semicolony.cron_read_action_relay_outbox AS outbox
|
|
160
|
+
WHERE outbox.idempotency_key = p_idempotency_key;
|
|
161
|
+
|
|
162
|
+
IF NOT FOUND THEN
|
|
163
|
+
RETURN QUERY SELECT 'missing'::text, NULL::text, NULL::text;
|
|
164
|
+
ELSIF stored_digest <> p_request_digest THEN
|
|
165
|
+
RETURN QUERY SELECT 'digest_mismatch'::text, NULL::text, NULL::text;
|
|
166
|
+
ELSIF stored_state = 'pending' THEN
|
|
167
|
+
RETURN QUERY SELECT 'pending'::text, NULL::text, NULL::text;
|
|
168
|
+
ELSE
|
|
169
|
+
RETURN QUERY SELECT 'completed'::text, stored_outcome, stored_error;
|
|
170
|
+
END IF;
|
|
171
|
+
END;
|
|
172
|
+
$$;
|
|
173
|
+
|
|
174
|
+
CREATE OR REPLACE FUNCTION semicolony.hive_cron_complete_outbox(
|
|
175
|
+
p_job_id text,
|
|
176
|
+
p_idempotency_key text,
|
|
177
|
+
p_request_digest text,
|
|
178
|
+
p_outcome_status text,
|
|
179
|
+
p_error_code text
|
|
180
|
+
)
|
|
181
|
+
RETURNS TABLE (
|
|
182
|
+
mutation_status text,
|
|
183
|
+
outcome_status text,
|
|
184
|
+
error_code text
|
|
185
|
+
)
|
|
186
|
+
LANGUAGE plpgsql
|
|
187
|
+
SECURITY DEFINER
|
|
188
|
+
SET search_path = pg_catalog
|
|
189
|
+
AS $$
|
|
190
|
+
DECLARE
|
|
191
|
+
completed_count integer;
|
|
192
|
+
key_prefix text := 'cron:' || p_job_id || ':';
|
|
193
|
+
BEGIN
|
|
194
|
+
IF p_job_id IS NULL
|
|
195
|
+
OR p_idempotency_key IS NULL
|
|
196
|
+
OR p_request_digest !~ '^[a-f0-9]{64}$'
|
|
197
|
+
OR pg_catalog.char_length(p_idempotency_key)
|
|
198
|
+
IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
|
|
199
|
+
OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
|
|
200
|
+
IS DISTINCT FROM key_prefix
|
|
201
|
+
OR pg_catalog.right(p_idempotency_key, 24)
|
|
202
|
+
!~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
|
|
203
|
+
OR NOT (
|
|
204
|
+
(p_outcome_status IN ('delivered', 'suppressed') AND p_error_code IS NULL)
|
|
205
|
+
OR (p_outcome_status = 'failed' AND p_error_code = 'delivery_failed')
|
|
206
|
+
)
|
|
207
|
+
OR NOT EXISTS (
|
|
208
|
+
SELECT 1
|
|
209
|
+
FROM semicolony.cron_capability_templates AS template
|
|
210
|
+
WHERE template.bot_id = 'semi'
|
|
211
|
+
AND template.job_id = p_job_id
|
|
212
|
+
AND template.runtime_source = 'hive-cron-v2'
|
|
213
|
+
AND template.action_kind IS NOT NULL
|
|
214
|
+
AND template.action_version IS NOT NULL
|
|
215
|
+
AND template.action_contract_digest IS NOT NULL
|
|
216
|
+
) THEN
|
|
217
|
+
RETURN;
|
|
218
|
+
END IF;
|
|
219
|
+
|
|
220
|
+
UPDATE semicolony.cron_read_action_relay_outbox AS outbox
|
|
221
|
+
SET state = 'completed',
|
|
222
|
+
outcome_status = p_outcome_status,
|
|
223
|
+
error_code = p_error_code,
|
|
224
|
+
completed_at = pg_catalog.clock_timestamp()
|
|
225
|
+
WHERE outbox.idempotency_key = p_idempotency_key
|
|
226
|
+
AND outbox.request_digest = p_request_digest
|
|
227
|
+
AND outbox.state = 'pending'
|
|
228
|
+
;
|
|
229
|
+
GET DIAGNOSTICS completed_count = ROW_COUNT;
|
|
230
|
+
|
|
231
|
+
IF completed_count = 1 THEN
|
|
232
|
+
RETURN QUERY SELECT 'completed'::text, p_outcome_status, p_error_code;
|
|
233
|
+
ELSE
|
|
234
|
+
RETURN QUERY SELECT 'not_completed'::text, NULL::text, NULL::text;
|
|
235
|
+
END IF;
|
|
236
|
+
END;
|
|
237
|
+
$$;
|
|
238
|
+
|
|
239
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_terminal_outbox() FROM PUBLIC, sc_app;
|
|
240
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_put_outbox(text, text, text) FROM PUBLIC, sc_app;
|
|
241
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_get_outbox(text, text, text) FROM PUBLIC, sc_app;
|
|
242
|
+
REVOKE ALL ON FUNCTION semicolony.hive_cron_complete_outbox(text, text, text, text, text)
|
|
243
|
+
FROM PUBLIC, sc_app;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Hive cron v2 retirement evidence must be unique across arbitrary commitment IDs.
|
|
2
|
+
-- The migration runner supplies the transaction boundary; do not add one here.
|
|
3
|
+
|
|
4
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_hive_cron_evidence_idempotency
|
|
5
|
+
ON semicolony.bot_commitments ((metadata->>'idempotency_key'))
|
|
6
|
+
WHERE bot_id = 'semi'
|
|
7
|
+
AND source_type = 'cron'
|
|
8
|
+
AND runtime_source = 'hive-cron-v2'
|
|
9
|
+
AND metadata->>'cron_parity_runtime' = 'hive-cron-v2';
|