primarypath-server 1.0.0__py3-none-any.whl

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.
Files changed (94) hide show
  1. app/__init__.py +351 -0
  2. app/_version.py +8 -0
  3. app/agents.py +150 -0
  4. app/alembic.ini +37 -0
  5. app/auth.py +714 -0
  6. app/chat.py +122 -0
  7. app/config.py +258 -0
  8. app/costgov.py +345 -0
  9. app/db.py +56 -0
  10. app/faults.py +26 -0
  11. app/grants.py +122 -0
  12. app/idempotency.py +58 -0
  13. app/jcs.py +117 -0
  14. app/ledger/__init__.py +41 -0
  15. app/ledger/fold.py +74 -0
  16. app/ledger/hashing.py +19 -0
  17. app/ledger/projection.py +389 -0
  18. app/ledger/store.py +1042 -0
  19. app/ledger/verify.py +31 -0
  20. app/main.py +50 -0
  21. app/maintenance.py +56 -0
  22. app/metrics.py +89 -0
  23. app/migrations/env.py +20 -0
  24. app/migrations/script.py.mako +23 -0
  25. app/migrations/versions/0001_baseline.py +19 -0
  26. app/migrations/versions/0002_events_and_projection.py +78 -0
  27. app/migrations/versions/0003_slack_messages.py +27 -0
  28. app/migrations/versions/0004_sessions.py +31 -0
  29. app/migrations/versions/0005_siem_cursor.py +26 -0
  30. app/migrations/versions/0006_teams_messages.py +27 -0
  31. app/migrations/versions/0007_tenant_scoping.py +50 -0
  32. app/migrations/versions/0008_policy_view.py +39 -0
  33. app/migrations/versions/0009_api_tokens.py +37 -0
  34. app/migrations/versions/0010_session_groups.py +23 -0
  35. app/migrations/versions/0011_budgets_and_evals.py +75 -0
  36. app/migrations/versions/0012_run_event_ts_index.py +23 -0
  37. app/migrations/versions/0013_agent_chains.py +102 -0
  38. app/migrations/versions/0014_dual_control.py +33 -0
  39. app/migrations/versions/0015_sampled_mode.py +24 -0
  40. app/migrations/versions/0016_sampling_salt.py +25 -0
  41. app/migrations/versions/0017_live_tokens_index.py +25 -0
  42. app/migrations/versions/0018_webhook_outbox.py +47 -0
  43. app/migrations/versions/0019_usage_view_and_rollout_index.py +68 -0
  44. app/migrations/versions/0020_policy_metadata_and_lifecycle.py +67 -0
  45. app/migrations/versions/0021_execution_grants.py +59 -0
  46. app/migrations/versions/0022_digest_alg_and_executor_instance.py +45 -0
  47. app/migrations/versions/0023_idempotency_keys.py +38 -0
  48. app/migrations/versions/0024_approvals_view_keyset_index.py +26 -0
  49. app/netguard.py +73 -0
  50. app/obslog.py +105 -0
  51. app/policy.py +390 -0
  52. app/ratelimit.py +78 -0
  53. app/routes/__init__.py +0 -0
  54. app/routes/admin.py +312 -0
  55. app/routes/agents.py +111 -0
  56. app/routes/approvals.py +576 -0
  57. app/routes/audit.py +213 -0
  58. app/routes/costgov.py +150 -0
  59. app/routes/health.py +53 -0
  60. app/routes/policy.py +72 -0
  61. app/routes/runs.py +115 -0
  62. app/routes/slack.py +40 -0
  63. app/routes/teams.py +29 -0
  64. app/routes/traces.py +55 -0
  65. app/schemas.py +248 -0
  66. app/security.py +203 -0
  67. app/siem.py +248 -0
  68. app/signing.py +342 -0
  69. app/sla.py +234 -0
  70. app/slack.py +282 -0
  71. app/teams.py +276 -0
  72. app/tokens.py +117 -0
  73. app/traces.py +309 -0
  74. app/web-dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  75. app/web-dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  76. app/web-dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  77. app/web-dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  78. app/web-dist/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
  79. app/web-dist/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
  80. app/web-dist/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
  81. app/web-dist/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
  82. app/web-dist/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
  83. app/web-dist/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
  84. app/web-dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  85. app/web-dist/assets/index-B_JDe_G2.js +10 -0
  86. app/web-dist/assets/index-J7J-YtwM.css +1 -0
  87. app/web-dist/index.html +24 -0
  88. app/webhooks.py +405 -0
  89. primarypath_server-1.0.0.dist-info/METADATA +35 -0
  90. primarypath_server-1.0.0.dist-info/RECORD +94 -0
  91. primarypath_server-1.0.0.dist-info/WHEEL +5 -0
  92. primarypath_server-1.0.0.dist-info/entry_points.txt +3 -0
  93. primarypath_server-1.0.0.dist-info/licenses/LICENSE +202 -0
  94. primarypath_server-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,75 @@
1
+ """budget_view (folded budgets/quotas) + eval/gate history projections.
2
+
3
+ All three are rebuildable caches of chained events (budget_set,
4
+ eval_recorded, gate_decision) — same discipline as policy_view/approvals_view.
5
+ Spend is NOT stored: it is folded live from run_event usage (no cached
6
+ counter that can drift). Per tenant.
7
+
8
+ Revision ID: 0011
9
+ Revises: 0010
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from alembic import op
14
+
15
+ revision = "0011"
16
+ down_revision = "0010"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.execute("""
23
+ CREATE TABLE budget_view (
24
+ tenant_id TEXT NOT NULL,
25
+ scope TEXT NOT NULL, -- 'tenant' | 'action'
26
+ key TEXT NOT NULL, -- action name, or '*' for the tenant
27
+ max_usd DOUBLE PRECISION,
28
+ max_tokens BIGINT,
29
+ window_kind TEXT NOT NULL, -- 'per_run' | 'daily' | 'monthly'
30
+ -- ('window' is a reserved keyword)
31
+ on_exceed TEXT NOT NULL, -- 'gate' | 'deny'
32
+ updated_by TEXT,
33
+ reason TEXT,
34
+ updated_ts_ms BIGINT NOT NULL,
35
+ PRIMARY KEY (tenant_id, scope, key)
36
+ )
37
+ """)
38
+ # Latest gate verdict per (tenant, agent, version): a fold of gate_decision
39
+ # (an override is just a newer gate_decision, last-write-wins).
40
+ op.execute("""
41
+ CREATE TABLE gate_view (
42
+ tenant_id TEXT NOT NULL,
43
+ agent TEXT NOT NULL,
44
+ version TEXT NOT NULL,
45
+ passed BOOLEAN NOT NULL,
46
+ baseline_version TEXT,
47
+ blocks JSONB NOT NULL DEFAULT '[]'::jsonb,
48
+ override BOOLEAN NOT NULL DEFAULT false,
49
+ decided_by TEXT,
50
+ reason TEXT,
51
+ decided_ts_ms BIGINT NOT NULL,
52
+ PRIMARY KEY (tenant_id, agent, version)
53
+ )
54
+ """)
55
+ # Append-only history rows for eval profiles and gate decisions, for the
56
+ # console history view (the chain remains the source of truth).
57
+ op.execute("""
58
+ CREATE TABLE eval_history (
59
+ id BIGSERIAL PRIMARY KEY,
60
+ tenant_id TEXT NOT NULL,
61
+ kind TEXT NOT NULL, -- 'eval' | 'gate'
62
+ agent TEXT NOT NULL,
63
+ version TEXT NOT NULL,
64
+ detail JSONB NOT NULL,
65
+ ts_ms BIGINT NOT NULL
66
+ )
67
+ """)
68
+ op.execute("CREATE INDEX eval_history_lookup_idx "
69
+ "ON eval_history (tenant_id, agent, version)")
70
+
71
+
72
+ def downgrade() -> None:
73
+ op.execute("DROP TABLE eval_history")
74
+ op.execute("DROP TABLE gate_view")
75
+ op.execute("DROP TABLE budget_view")
@@ -0,0 +1,23 @@
1
+ """Index the cost-fold path: daily/monthly spend scans filter run_events by
2
+ (tenant_id, ts_ms). (per_run already uses events_tenant_run_idx from 0007.)
3
+
4
+ Revision ID: 0012
5
+ Revises: 0011
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from alembic import op
10
+
11
+ revision = "0012"
12
+ down_revision = "0011"
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ op.execute("CREATE INDEX events_tenant_type_ts_idx "
19
+ "ON events (tenant_id, type, ts_ms)")
20
+
21
+
22
+ def downgrade() -> None:
23
+ op.execute("DROP INDEX events_tenant_type_ts_idx")
@@ -0,0 +1,102 @@
1
+ """F24: chain-per-agent. The chain scope generalizes from tenant to
2
+ (tenant_id, chain_id): activity events live on per-agent chains
3
+ (chain_id = agent_id), control-plane events on a per-tenant governance
4
+ chain (chain_id = '__governance__'). Every event row carries the
5
+ agent_id/unit stamped from the service token; api_tokens carries the
6
+ agent-to-unit map. evidence_view / agents_view / config_view are folds of
7
+ governance events (rebuildable caches, like approvals_view).
8
+
9
+ Existing events adopt chain_id = 'default' (and agent_id = 'default'), so
10
+ a one-agent install keeps its F20 tenant chain byte-for-byte.
11
+
12
+ `rowid` is a global ingest-order identity used ONLY for cross-chain reads
13
+ (SIEM cursor, whole-log listings). It is not part of any hash; per-chain
14
+ `seq`/`prev_hash` remain the tamper-evidence.
15
+
16
+ Revision ID: 0013
17
+ Revises: 0012
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from alembic import op
22
+
23
+ revision = "0013"
24
+ down_revision = "0012"
25
+ branch_labels = None
26
+ depends_on = None
27
+
28
+
29
+ def upgrade() -> None:
30
+ # ADD COLUMN ... DEFAULT is metadata-only for existing rows (no row
31
+ # rewrite, so the append-only trigger is not in play). The identity
32
+ # column backfills in physical (insert) order.
33
+ op.execute("ALTER TABLE events ADD COLUMN chain_id TEXT NOT NULL "
34
+ "DEFAULT 'default'")
35
+ op.execute("ALTER TABLE events ADD COLUMN agent_id TEXT DEFAULT 'default'")
36
+ op.execute("ALTER TABLE events ALTER COLUMN agent_id DROP DEFAULT")
37
+ op.execute("ALTER TABLE events ADD COLUMN unit TEXT")
38
+ op.execute("ALTER TABLE events ADD COLUMN rowid BIGINT "
39
+ "GENERATED ALWAYS AS IDENTITY")
40
+ op.execute("ALTER TABLE events DROP CONSTRAINT events_pkey")
41
+ op.execute("ALTER TABLE events ADD PRIMARY KEY (tenant_id, chain_id, seq)")
42
+ op.execute("CREATE INDEX events_tenant_rowid_idx ON events "
43
+ "(tenant_id, rowid)")
44
+
45
+ op.execute("ALTER TABLE approvals_view ADD COLUMN agent_id TEXT NOT NULL "
46
+ "DEFAULT 'default'")
47
+ op.execute("ALTER TABLE approvals_view ADD COLUMN unit TEXT")
48
+
49
+ # the agent-to-unit map rides the service token (resolved server-side,
50
+ # never from the request body — the F20 tenant discipline)
51
+ op.execute("ALTER TABLE api_tokens ADD COLUMN agent_id TEXT")
52
+ op.execute("ALTER TABLE api_tokens ADD COLUMN unit TEXT")
53
+
54
+ # cursor moves from per-chain seq to global ingest order; reset is safe
55
+ # (redelivery is deduped at the SIEM)
56
+ op.execute("DROP TABLE siem_cursor")
57
+ op.execute("""
58
+ CREATE TABLE siem_cursor (
59
+ tenant_id TEXT PRIMARY KEY,
60
+ last_rowid BIGINT NOT NULL
61
+ )
62
+ """)
63
+
64
+ # folds of governance events (rebuildable caches, never the truth)
65
+ op.execute("""
66
+ CREATE TABLE agents_view (
67
+ tenant_id TEXT NOT NULL,
68
+ agent TEXT NOT NULL,
69
+ unit TEXT,
70
+ updated_by TEXT,
71
+ reason TEXT,
72
+ updated_ts_ms BIGINT NOT NULL,
73
+ PRIMARY KEY (tenant_id, agent)
74
+ )
75
+ """)
76
+ op.execute("""
77
+ CREATE TABLE evidence_view (
78
+ tenant_id TEXT NOT NULL,
79
+ agent TEXT NOT NULL,
80
+ signing_backend TEXT NOT NULL,
81
+ key_id TEXT,
82
+ capture TEXT NOT NULL,
83
+ retention_days BIGINT,
84
+ updated_by TEXT,
85
+ reason TEXT,
86
+ updated_ts_ms BIGINT NOT NULL,
87
+ PRIMARY KEY (tenant_id, agent)
88
+ )
89
+ """)
90
+ op.execute("""
91
+ CREATE TABLE config_view (
92
+ tenant_id TEXT NOT NULL,
93
+ key TEXT NOT NULL,
94
+ value JSONB NOT NULL,
95
+ updated_ts_ms BIGINT NOT NULL,
96
+ PRIMARY KEY (tenant_id, key)
97
+ )
98
+ """)
99
+
100
+
101
+ def downgrade() -> None:
102
+ raise NotImplementedError("per-agent chains are a one-way seam")
@@ -0,0 +1,33 @@
1
+ """Dual control (four-eyes): a per-action policy can demand a second,
2
+ different approver. The first approval appends an `approval_endorsed`
3
+ event (the approval STAYS pending); only a different reviewer's approval
4
+ appends `approval_granted`. Both humans are on the chain.
5
+
6
+ policy_view gains the flag; approvals_view caches the endorsement so the
7
+ queue can show "1 of 2" without folding the log per row. Both are pure
8
+ projections — the log stays truth and a rebuild reproduces them.
9
+
10
+ Revision ID: 0014
11
+ Revises: 0013
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from alembic import op
16
+
17
+ revision = "0014"
18
+ down_revision = "0013"
19
+ branch_labels = None
20
+ depends_on = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.execute("ALTER TABLE policy_view ADD COLUMN require_second_approval "
25
+ "BOOLEAN NOT NULL DEFAULT FALSE")
26
+ op.execute("ALTER TABLE approvals_view ADD COLUMN endorsed_by TEXT")
27
+ op.execute("ALTER TABLE approvals_view ADD COLUMN endorsed_ts_ms BIGINT")
28
+
29
+
30
+ def downgrade() -> None:
31
+ op.execute("ALTER TABLE approvals_view DROP COLUMN endorsed_ts_ms")
32
+ op.execute("ALTER TABLE approvals_view DROP COLUMN endorsed_by")
33
+ op.execute("ALTER TABLE policy_view DROP COLUMN require_second_approval")
@@ -0,0 +1,24 @@
1
+ """Sampled autonomy: policy mode 'sampled' auto-grants most of an action's
2
+ proposals but routes a deterministic 1-in-N to a human. policy_view gains
3
+ the sampling denominator; 0 means not applicable (non-sampled modes).
4
+
5
+ Revision ID: 0015
6
+ Revises: 0014
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from alembic import op
11
+
12
+ revision = "0015"
13
+ down_revision = "0014"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ op.execute("ALTER TABLE policy_view ADD COLUMN sample_n INTEGER "
20
+ "NOT NULL DEFAULT 0")
21
+
22
+
23
+ def downgrade() -> None:
24
+ op.execute("ALTER TABLE policy_view DROP COLUMN sample_n")
@@ -0,0 +1,25 @@
1
+ """Sampling salt: the sampled-mode 1-in-N pick becomes an HMAC keyed by a
2
+ per-tenant server secret, so an agent (who chooses run_id:call_id) cannot
3
+ grind ids that are never sampled. Operational, not chained; disclosed in
4
+ the admin-gated export so an auditor can still recompute every pick.
5
+
6
+ Revision ID: 0016
7
+ Revises: 0015
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from alembic import op
12
+
13
+ revision = "0016"
14
+ down_revision = "0015"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ op.execute("CREATE TABLE sampling_salt ("
21
+ "tenant_id TEXT PRIMARY KEY, salt TEXT NOT NULL)")
22
+
23
+
24
+ def downgrade() -> None:
25
+ op.execute("DROP TABLE sampling_salt")
@@ -0,0 +1,25 @@
1
+ """Partial index over live issued tokens: the token-plane posture check
2
+ (does any live service/admin token exist?) runs on the authentication
3
+ boundary of every request in issued-token-only deployments, uncached so
4
+ every replica sees the same answer. Keep that EXISTS an index probe.
5
+
6
+ Revision ID: 0017
7
+ Revises: 0016
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from alembic import op
12
+
13
+ revision = "0017"
14
+ down_revision = "0016"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ op.execute("CREATE INDEX IF NOT EXISTS api_tokens_live_kind_idx "
21
+ "ON api_tokens (kind) WHERE revoked_ts_ms IS NULL")
22
+
23
+
24
+ def downgrade() -> None:
25
+ op.execute("DROP INDEX IF EXISTS api_tokens_live_kind_idx")
@@ -0,0 +1,47 @@
1
+ """Webhook outbox: a callback is written in the SAME transaction as the
2
+ decision it announces, then delivered by a leased worker with capped
3
+ backoff, a dead-letter state, and admin replay. At-least-once for real:
4
+ a crash after the decision commits can no longer lose the callback,
5
+ because the callback IS committed with it. Operational state, not part
6
+ of the audit chain (the decision event is the truth; this is transport).
7
+
8
+ Revision ID: 0018
9
+ Revises: 0017
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from alembic import op
14
+
15
+ revision = "0018"
16
+ down_revision = "0017"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.execute("""
23
+ CREATE TABLE webhook_outbox (
24
+ id BIGSERIAL PRIMARY KEY,
25
+ tenant_id TEXT NOT NULL DEFAULT 'default',
26
+ kind TEXT NOT NULL,
27
+ approval_id TEXT,
28
+ event_hash TEXT,
29
+ url TEXT NOT NULL,
30
+ payload_json JSONB NOT NULL,
31
+ status TEXT NOT NULL DEFAULT 'pending',
32
+ attempts INTEGER NOT NULL DEFAULT 0,
33
+ created_ts_ms BIGINT NOT NULL,
34
+ next_attempt_ts_ms BIGINT NOT NULL,
35
+ delivered_ts_ms BIGINT,
36
+ last_status INTEGER,
37
+ last_error TEXT
38
+ )
39
+ """)
40
+ op.execute("CREATE INDEX webhook_outbox_due_idx ON webhook_outbox "
41
+ "(next_attempt_ts_ms, id) WHERE status = 'pending'")
42
+ op.execute("CREATE INDEX webhook_outbox_tenant_idx ON webhook_outbox "
43
+ "(tenant_id, status, id DESC)")
44
+
45
+
46
+ def downgrade() -> None:
47
+ op.execute("DROP TABLE IF EXISTS webhook_outbox")
@@ -0,0 +1,68 @@
1
+ """usage_view: a rebuildable projection of model_decision usage (one row
2
+ per decision: model, tokens, the tools it called), so budgets and the
3
+ spend report are SQL aggregates over an indexed table instead of an
4
+ application-side fold of every run_event in the window. Backfilled from
5
+ the log here; kept current by apply_event; reproduced by
6
+ rebuild_projection like every other view.
7
+
8
+ Also indexes approvals_view for the rollout track record, which is now
9
+ a GROUP BY over (tenant, tool) within an explicit lookback window.
10
+
11
+ Revision ID: 0019
12
+ Revises: 0018
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from alembic import op
17
+
18
+ revision = "0019"
19
+ down_revision = "0018"
20
+ branch_labels = None
21
+ depends_on = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ op.execute("""
26
+ CREATE TABLE usage_view (
27
+ id BIGSERIAL PRIMARY KEY,
28
+ tenant_id TEXT NOT NULL,
29
+ run_id TEXT,
30
+ ts_ms BIGINT NOT NULL,
31
+ model TEXT NOT NULL,
32
+ input_tokens BIGINT NOT NULL DEFAULT 0,
33
+ output_tokens BIGINT NOT NULL DEFAULT 0,
34
+ tools TEXT[] NOT NULL DEFAULT '{}'
35
+ )
36
+ """)
37
+ op.execute("CREATE INDEX usage_view_tenant_ts_idx ON usage_view "
38
+ "(tenant_id, ts_ms)")
39
+ op.execute("CREATE INDEX usage_view_tenant_run_idx ON usage_view "
40
+ "(tenant_id, run_id)")
41
+ op.execute("CREATE INDEX usage_view_tools_idx ON usage_view USING GIN (tools)")
42
+ # backfill from the log, in append order
43
+ op.execute("""
44
+ INSERT INTO usage_view
45
+ (tenant_id, run_id, ts_ms, model, input_tokens, output_tokens, tools)
46
+ SELECT tenant_id, run_id, ts_ms,
47
+ coalesce(p->'payload'->>'model', 'unknown'),
48
+ coalesce(nullif(p->'payload'->'usage'->>'input_tokens', '')::numeric::bigint, 0),
49
+ coalesce(nullif(p->'payload'->'usage'->>'output_tokens', '')::numeric::bigint, 0),
50
+ coalesce(ARRAY(
51
+ SELECT c->>'name'
52
+ FROM jsonb_array_elements(
53
+ CASE WHEN jsonb_typeof(p->'payload'->'tool_calls') = 'array'
54
+ THEN p->'payload'->'tool_calls' ELSE '[]'::jsonb END) AS c
55
+ WHERE c->>'name' IS NOT NULL), '{}')
56
+ FROM (SELECT tenant_id, run_id, ts_ms, rowid,
57
+ (payload_json::jsonb)->'event' AS p
58
+ FROM events WHERE type = 'run_event') AS e
59
+ WHERE p->>'type' = 'model_decision'
60
+ ORDER BY rowid
61
+ """)
62
+ op.execute("CREATE INDEX approvals_view_tenant_tool_ts_idx ON approvals_view "
63
+ "(tenant_id, tool, requested_ts_ms)")
64
+
65
+
66
+ def downgrade() -> None:
67
+ op.execute("DROP INDEX IF EXISTS approvals_view_tenant_tool_ts_idx")
68
+ op.execute("DROP TABLE IF EXISTS usage_view")
@@ -0,0 +1,67 @@
1
+ """Server-authored risk metadata and a fuller approval lifecycle.
2
+
3
+ policy_view gains risk (low/medium/high/critical), blast_radius (free
4
+ text the admin authors) and version (the first 12 hex of the policy_set
5
+ event's chain hash — the exact rule a request was evaluated against).
6
+
7
+ approvals_view gains the request-time snapshot the console shows and the
8
+ server enforces on: disposition, action_digest, policy (the rule as it
9
+ stood, with its version), rule_result (why it was gated or not), and
10
+ supersedes. status may now also be cancelled / expired / superseded /
11
+ changes_requested. Backfilled from the log.
12
+
13
+ Revision ID: 0020
14
+ Revises: 0019
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from alembic import op
19
+
20
+ revision = "0020"
21
+ down_revision = "0019"
22
+ branch_labels = None
23
+ depends_on = None
24
+
25
+
26
+ def upgrade() -> None:
27
+ op.execute("ALTER TABLE policy_view ADD COLUMN risk TEXT NOT NULL "
28
+ "DEFAULT 'medium'")
29
+ op.execute("ALTER TABLE policy_view ADD COLUMN blast_radius TEXT")
30
+ op.execute("ALTER TABLE policy_view ADD COLUMN version TEXT")
31
+ op.execute("""
32
+ UPDATE policy_view pv SET
33
+ version = left(e.hash, 12),
34
+ risk = coalesce(e.payload_json->>'risk',
35
+ CASE WHEN pv.reversible THEN 'medium' ELSE 'high' END),
36
+ blast_radius = e.payload_json->>'blast_radius'
37
+ FROM (
38
+ SELECT DISTINCT ON (tenant_id, payload_json->>'action')
39
+ tenant_id, payload_json->>'action' AS action, hash, payload_json
40
+ FROM events WHERE type = 'policy_set'
41
+ ORDER BY tenant_id, payload_json->>'action', rowid DESC
42
+ ) e
43
+ WHERE e.tenant_id = pv.tenant_id AND e.action = pv.action
44
+ """)
45
+ for col, typ in (("disposition", "TEXT"), ("action_digest", "TEXT"),
46
+ ("policy", "JSONB"), ("rule_result", "JSONB"),
47
+ ("supersedes", "TEXT")):
48
+ op.execute(f"ALTER TABLE approvals_view ADD COLUMN {col} {typ}")
49
+ op.execute("""
50
+ UPDATE approvals_view av SET
51
+ disposition = e.payload_json->>'disposition',
52
+ action_digest = e.payload_json->>'action_digest',
53
+ policy = e.payload_json->'policy',
54
+ rule_result = e.payload_json->'rule_result',
55
+ supersedes = e.payload_json->>'supersedes'
56
+ FROM events e
57
+ WHERE e.type = 'approval_requested' AND e.tenant_id = av.tenant_id
58
+ AND e.payload_json->>'approval_id' = av.approval_id
59
+ """)
60
+
61
+
62
+ def downgrade() -> None:
63
+ for col in ("disposition", "action_digest", "policy", "rule_result",
64
+ "supersedes"):
65
+ op.execute(f"ALTER TABLE approvals_view DROP COLUMN {col}")
66
+ for col in ("risk", "blast_radius", "version"):
67
+ op.execute(f"ALTER TABLE policy_view DROP COLUMN {col}")
@@ -0,0 +1,59 @@
1
+ """Signed execution grants (the approve→execute seam).
2
+
3
+ approvals_view caches the grant stamped into `approval_granted` (nonce,
4
+ issue/expiry, key id, signature) and its single redemption
5
+ (`grant_redeemed`: when, by which executor). Backfilled from the log;
6
+ kept current by apply_event; reproduced by rebuild_projection.
7
+
8
+ Revision ID: 0021
9
+ Revises: 0020
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from alembic import op
14
+
15
+ revision = "0021"
16
+ down_revision = "0020"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+ COLUMNS = (("grant_nonce", "TEXT"), ("grant_issued_ms", "BIGINT"),
21
+ ("grant_expires_ms", "BIGINT"), ("grant_key_id", "TEXT"),
22
+ ("grant_signature", "TEXT"), ("grant_redeemed_ts_ms", "BIGINT"),
23
+ ("grant_redeemed_by", "TEXT"))
24
+
25
+
26
+ def upgrade() -> None:
27
+ for col, typ in COLUMNS:
28
+ op.execute(f"ALTER TABLE approvals_view ADD COLUMN {col} {typ}")
29
+ op.execute("""
30
+ UPDATE approvals_view av SET
31
+ grant_nonce = e.payload_json->'grant'->>'nonce',
32
+ grant_issued_ms = (e.payload_json->'grant'->>'issued_ms')::bigint,
33
+ grant_expires_ms = (e.payload_json->'grant'->>'expires_ms')::bigint,
34
+ grant_key_id = e.payload_json->'grant'->>'key_id',
35
+ grant_signature = e.payload_json->'grant'->>'signature'
36
+ FROM events e
37
+ WHERE e.type = 'approval_granted' AND e.tenant_id = av.tenant_id
38
+ AND e.payload_json->>'approval_id' = av.approval_id
39
+ AND e.payload_json->'grant'->>'nonce' IS NOT NULL
40
+ """)
41
+ op.execute("""
42
+ UPDATE approvals_view av SET
43
+ grant_redeemed_ts_ms = r.ts_ms,
44
+ grant_redeemed_by = r.payload_json->>'executor'
45
+ FROM (
46
+ SELECT DISTINCT ON (tenant_id, payload_json->>'approval_id')
47
+ tenant_id, payload_json->>'approval_id' AS approval_id,
48
+ ts_ms, payload_json
49
+ FROM events WHERE type = 'grant_redeemed'
50
+ ORDER BY tenant_id, payload_json->>'approval_id', rowid
51
+ ) r
52
+ WHERE r.tenant_id = av.tenant_id AND r.approval_id = av.approval_id
53
+ AND r.payload_json->>'nonce' = av.grant_nonce
54
+ """)
55
+
56
+
57
+ def downgrade() -> None:
58
+ for col, _ in COLUMNS:
59
+ op.execute(f"ALTER TABLE approvals_view DROP COLUMN {col}")
@@ -0,0 +1,45 @@
1
+ """Cross-language action digests and credential-bound executors.
2
+
3
+ approvals_view gains digest_alg ("jcs" for approvals whose action_digest
4
+ is RFC 8785 canonical; NULL for pre-0.3 Python-canonical digests) and
5
+ grant_redeemed_instance (the executor's self-asserted name, kept apart
6
+ from grant_redeemed_by, which is now the credential-derived identity).
7
+ Backfilled from the log.
8
+
9
+ Revision ID: 0022
10
+ Revises: 0021
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from alembic import op
15
+
16
+ revision = "0022"
17
+ down_revision = "0021"
18
+ branch_labels = None
19
+ depends_on = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ op.execute("ALTER TABLE approvals_view ADD COLUMN digest_alg TEXT")
24
+ op.execute("ALTER TABLE approvals_view ADD COLUMN grant_redeemed_instance TEXT")
25
+ op.execute("""
26
+ UPDATE approvals_view av SET digest_alg = e.payload_json->>'digest_alg'
27
+ FROM events e
28
+ WHERE e.type = 'approval_requested' AND e.tenant_id = av.tenant_id
29
+ AND e.payload_json->>'approval_id' = av.approval_id
30
+ AND e.payload_json->>'digest_alg' IS NOT NULL
31
+ """)
32
+ op.execute("""
33
+ UPDATE approvals_view av SET
34
+ grant_redeemed_instance = r.payload_json->>'executor_instance'
35
+ FROM events r
36
+ WHERE r.type = 'grant_redeemed' AND r.tenant_id = av.tenant_id
37
+ AND r.payload_json->>'approval_id' = av.approval_id
38
+ AND r.payload_json->>'nonce' = av.grant_nonce
39
+ AND r.payload_json->>'executor_instance' IS NOT NULL
40
+ """)
41
+
42
+
43
+ def downgrade() -> None:
44
+ op.execute("ALTER TABLE approvals_view DROP COLUMN grant_redeemed_instance")
45
+ op.execute("ALTER TABLE approvals_view DROP COLUMN digest_alg")
@@ -0,0 +1,38 @@
1
+ """Operation idempotency keys.
2
+
3
+ A mutating request may carry `Idempotency-Key`. The key is recorded in
4
+ the SAME transaction as the events it produces, with a fingerprint of
5
+ (tenant, credential, method, path, body) and the response it earned, so
6
+ a retry after a lost response returns that response instead of writing
7
+ again, and a crash between commit and response cannot duplicate
8
+ anything. Operational state, not part of the audit chain.
9
+
10
+ Revision ID: 0023
11
+ Revises: 0022
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from alembic import op
16
+
17
+ revision = "0023"
18
+ down_revision = "0022"
19
+ branch_labels = None
20
+ depends_on = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.execute("""
25
+ CREATE TABLE idempotency_keys (
26
+ tenant_id TEXT NOT NULL,
27
+ key TEXT NOT NULL,
28
+ fingerprint TEXT NOT NULL,
29
+ result JSONB NOT NULL,
30
+ created_ts_ms BIGINT NOT NULL,
31
+ PRIMARY KEY (tenant_id, key)
32
+ )
33
+ """)
34
+ op.execute("CREATE INDEX idempotency_keys_created_idx ON idempotency_keys (created_ts_ms)")
35
+
36
+
37
+ def downgrade() -> None:
38
+ op.execute("DROP TABLE idempotency_keys")
@@ -0,0 +1,26 @@
1
+ """approvals_view: an index for the list endpoint's total order.
2
+
3
+ The inbox pages the queue by (requested_ts_ms, approval_id), oldest or
4
+ newest first, with a keyset cursor. Without an index every page sorts the
5
+ tenant's whole view; with it a page is a range scan in either direction.
6
+
7
+ Revision ID: 0024
8
+ Revises: 0023
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from alembic import op
13
+
14
+ revision = "0024"
15
+ down_revision = "0023"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ op.execute("CREATE INDEX approvals_view_tenant_ts_id_idx ON approvals_view "
22
+ "(tenant_id, requested_ts_ms, approval_id)")
23
+
24
+
25
+ def downgrade() -> None:
26
+ op.execute("DROP INDEX IF EXISTS approvals_view_tenant_ts_id_idx")