Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(KR-3 ST2): iso_link_* typed-edge tool family#10

Merged
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR3-iso-link-tools
May 20, 2026
Merged

feat(KR-3 ST2): iso_link_* typed-edge tool family#10
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR3-iso-link-tools

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

KR-3 ST2 ships the iso_link_* typed-edge tool family — 3 model-facing MCP tools over the relationlink substrate (ADR-0033/ADR-0034 typed edges).

Tool Purpose Write/Read
iso_link_create Declare a typed edge (21 V1 link_type vocabulary) write (deferred — 3 blockers)
iso_link_traverse Walk edges from a start node; recursive CTE; max_depth ≤ 3 read
iso_link_list_for_node List all active edges where the node is source or target read

Combined typed-graph surface is now 7 tools (4 iso_node_* + 3 iso_link_*) returned by get_tool_schemas.

Rule-3 STOP-gate findings during recon

Verified against packages/db/migrations/0058_relationlink.sql on substrate main 41ddc208:

  • Table is relationlink (bare, no schema prefix) — lives in public. Same workspace_id TEXT pattern as ST2/ST3 reads (not tenant_id).
  • No RLS on the tableworkspace_id filter in WHERE is the application-layer isolation. Direct asyncpg reads work without GUC.
  • link_type column is un-CHECK'd TEXT — V1 vocabulary (21 entries: 11 sea/idea + 10 platform-wide) is informational and lives in sb1-substrate-shapes/src/relationlink.ts. The JSON Schema enum on iso_link_create enforces it client-side; the application-layer per-pair gate config is the normative source of truth.
  • Three concurrent blockers gate the write path (new BUILD_DEVIATIONS entry D-kr3-st2-no-relationlink-write-mcp-tool):
    1. created_by_actor_kind CHECK lacks 'kora'. The CHECK covers 8 entries (operator, oracle, critic, claude_pm, hermes, platform_seal, platform_rollback, platform_session_expiry). Migration 0058 predates 'kora''s introduction (added to actor_registry in 0076). A Kora-side INSERT would fail.
    2. No Sea MCP write tool exposes the path. Same probe as ST3 scratchpad-write recon — only kora__propose_convention, kora__read_escalation_queue, kora__propose_policy_change registered. No kora__create_relationlink equivalent.
    3. chain_event_id UUID NOT NULL requires substrate-side SECDEF that emits + binds the chain event in one transaction (same pattern as kronicle.compact_scratchpad from Plan 02). Direct INSERT either fails (no chain_event_id) or, if filled in client-side, breaks the chain witness invariant.

Per ST2 spec § "Read vs write paths": "Writes → through Sea MCP (authorization + audit)". Deferred per the spec's BUILD_DEVIATIONS-or-defer pattern. PM coordinates the substrate-side bucket that addresses all three blockers in one shot.

What landed

File Change
plugins/memory/isokron/relationlink.py newRelationLinkRow, ReachableNode, RelationLinkWriteNotAvailableError, V1 link-type vocabulary constants (SEA_IDEA_LINK_TYPES, PLATFORM_WIDE_LINK_TYPES, V1_LINK_TYPES, RELATIONLINK_VALIDITY_STATES), canonical SQL (list + 2x recursive-CTE traverse), read_relationlink_for_node, traverse_relationlink, deferred create_relationlink. ~280 LOC.
plugins/memory/isokron/tools/iso_link.py new — 3 tool schemas + handlers + dispatcher. ~290 LOC.
plugins/memory/isokron/tools/__init__.py Combined ISO_TYPED_GRAPH_TOOL_SCHEMAS = ISO_NODE + ISO_LINK export; re-exports the link dispatcher.
plugins/memory/isokron/provider.py get_tool_schemas returns 7 tools via the combined export; handle_tool_call adds the iso_link_* prefix branch.
BUILD_DEVIATIONS.md New D-kr3-st2-no-relationlink-write-mcp-tool Open entry — all three blockers spelled out + closure condition.
tests/plugins/memory/test_iso_link_tools.py new — 22 tests (see test plan).
tests/plugins/memory/test_isokron_provider_skeleton.py test_tool_schemas_exposes_iso_node_familytest_tool_schemas_exposes_iso_typed_graph_family (7 names asserted).

Traverse SQL (recursive CTE per direction)

For outgoing:

WITH RECURSIVE walk AS (
  SELECT to_entity_id::text AS entity_id, to_entity_kind AS entity_kind,
         link_type AS via_link_type, 1 AS depth
  FROM relationlink
  WHERE workspace_id = $1 AND validity_state = 'active'
    AND from_entity_id = $2 AND link_type = ANY($3::text[])
  UNION ALL
  SELECT rl.to_entity_id::text, rl.to_entity_kind, rl.link_type, walk.depth + 1
  FROM walk
  JOIN relationlink rl ON rl.workspace_id = $1 AND rl.validity_state = 'active'
    AND rl.link_type = ANY($3::text[])
    AND rl.from_entity_id::text = walk.entity_id
  WHERE walk.depth < $4
)
SELECT DISTINCT entity_id, entity_kind, via_link_type, depth FROM walk

Mirror for incoming walks to_entity_id → from_entity_id. direction='both' issues both queries and the Python side dedupes by (entity_id, entity_kind, via_link_type) keeping MIN depth.

Capability checks

Reuses the iso_node assert_kora_can_perform stub (D-kr3-st1-capability-check-deferred). Per-tool capability mapping:

  • iso_link_createcap_sea_link_authoring
  • iso_link_traversecap_read_unfiltered_relationlink
  • iso_link_list_for_nodecap_read_unfiltered_relationlink

Test plan

22 new tests, all passing:

Schemas (6):

  • All 3 schemas well-formed (OpenAI function-call shape)
  • V1_LINK_TYPES = 11 sea/idea + 10 platform-wide = 21, no overlap
  • iso_link_create schema's link_type enum matches V1_LINK_TYPES tuple
  • iso_link_traverse schema caps max_depth at 3
  • iso_link_list_for_node schema caps limit at 100
  • Combined typed-graph surface is 7 tools

Read SQL (2):

  • read_relationlink_for_node binds (workspace_id, entity_id, limit); OR on from/to; filters validity_state='active'
  • List-for-node caps limit at MAX_LIST_LIMIT=100

Traverse (5):

  • Outgoing runs one query (anchors at from_entity_id)
  • direction='both' runs two queries (outgoing + incoming variants)
  • max_depth capped at 3
  • Rejects empty link_types (would sprawl)
  • Dedupes by MIN depth when same node reached via multiple paths
  • Rejects unknown direction values

Deferred write (1):

  • create_relationlink raises RelationLinkWriteNotAvailableError; message names all 3 blockers verbatim (operators can grep the message + see exactly what substrate needs to ship)

Tool handlers (8):

  • iso_link_create returns deferred envelope with the right deviation_id
  • iso_link_create rejects invalid link_type
  • iso_link_create rejects invalid node_kind
  • iso_link_traverse returns results from mocked CTE
  • iso_link_traverse validates link_types against V1 vocabulary
  • iso_link_list_for_node returns rows
  • iso_link_list_for_node enforces server-side limit cap
  • provider.handle_tool_call routes iso_link_* by prefix

Capability check (1):

  • Shared assert_kora_can_perform stub fires + logs D-kr3-st1-capability-check-deferred + the cap name on the iso_link traverse path (proves both families share the gate)

Gates

  • ty check7,337 diagnostics, zero-delta vs KR-3 ST1 baseline.
  • pytest tests/plugins/memory/294/294 passing (22 new ST2 + 22 ST1 + 250 pre-ST2).
  • Full suite via xdist (-n auto): 24,617 passed / 142 failed / 129 skipped vs KR-2 ST4 merge baseline (24,570/143/129): +47 passed, −1 fail. Same tests/tools/* + tests/skills/* + tests/tui_gateway/* xdist isolation noise; none touch plugins/memory/isokron/.

Rule-6 / BUILD_DEVIATIONS / Open asks

New deviation D-kr3-st2-no-relationlink-write-mcp-tool — 3 substrate blockers spelled out in BUILD_DEVIATIONS.md. PM needs to dispatch the substrate-side bucket that handles all three in one shot (CHECK extension + MCP tool + chain-event SECDEF).

Existing deviations carry forward (5 total open now):

Deviation Surface Closes when
D-kr2-st2-capability-matrix-mirror C2 Python mirror K-7
D-kr2-st3-no-scratchpad-write-mcp-tool scratchpad writes K-8
D-kr2-st4-no-chain-emit-mcp-tool chain event emit K-9
D-kr3-st1-capability-check-deferred assert_kora_can_perform stub KR-6 (CC#3 lane — cheap unlock)
D-kr3-st2-no-relationlink-write-mcp-tool relationlink writes substrate bucket (3 blockers in one)

Per PM's pipeline-order note: if no substrate buckets are ready to swap to after ST3, KR-6 is next on this lane — it's the cheapest unlock (~50 LOC; closes D-kr3-st1 without needing CC#1 substrate work).

Standing by for KR-3 ST3 (tool registration polish + Hermes memory deprecation + system prompt updates).

🤖 Generated with Claude Code

Three model-facing MCP tools over the relationlink substrate
(ADR-0033 / ADR-0034 typed edges):

* iso_link_create — declare a typed edge. Deferred behind a NEW
  RelationLinkWriteNotAvailableError with three concurrent blockers
  in substrate main (D-kr3-st2-no-relationlink-write-mcp-tool):
  (1) created_by_actor_kind CHECK lacks 'kora' (only 8 actor_kinds
      covered; Kora-side INSERT would fail);
  (2) no Sea MCP write tool exposes the path;
  (3) chain_event_id NOT NULL requires substrate-side SECDEF that
      emits + binds the chain event in one transaction.
  Direct INSERT bypasses all three guards — do NOT do that.

* iso_link_traverse — walk typed edges from a starting node, bounded
  by max_depth (≤3 per Tenet 2) and a list of allowed link_types.
  Recursive CTE per direction; 'outgoing' / 'incoming' / 'both'.
  Both runs two separate queries and merges via dedup-by-min-depth
  (same node reached via shorter path wins).

* iso_link_list_for_node — list all active edges where the node is
  either source or target. Single query (OR on from_entity_id /
  to_entity_id), capped at 100 rows.

Recon findings (verified against
packages/db/migrations/0058_relationlink.sql on substrate main):
* Table: relationlink (no schema prefix — lives in public).
* PK: link_id UUID. Workspace keying: workspace_id TEXT REFERENCES
  workspaces(id) — same pattern as ST2/ST3 reads (not tenant_id).
* No RLS on the table — workspace_id filter in WHERE is the
  application-layer isolation. Direct asyncpg reads without GUC.
* link_type column is un-CHECK'd TEXT; V1 vocabulary (21 entries —
  11 sea/idea + 10 platform-wide) is informational and lives in
  sb1-substrate-shapes/src/relationlink.ts as SEA_IDEA_LINK_TYPES +
  PLATFORM_WIDE_LINK_TYPES. The JSON Schema enum on iso_link_create
  enforces it client-side; the application-layer per-pair gate
  config is the normative source of truth.
* validity_state closed enum (active / superseded / disputed /
  tombstoned). All reads filter to 'active'.

Provider wiring:
* get_tool_schemas now returns 7 tools (4 iso_node_* + 3 iso_link_*)
  via the new ISO_TYPED_GRAPH_TOOL_SCHEMAS combined export.
* handle_tool_call dispatches iso_link_* by prefix; unknown tool
  names still fall through to the ABC default's clear-error path.

Capability checks reuse the iso_node assert_kora_can_perform stub
(D-kr3-st1-capability-check-deferred). The per-tool capability map:
iso_link_create → cap_sea_link_authoring; iso_link_traverse +
iso_link_list_for_node → cap_read_unfiltered_relationlink.

Tests (22 new):
* Schemas: 3 well-formed; V1_LINK_TYPES sanity (11+10=21, no
  overlap); ISO_LINK_CREATE_SCHEMA enum syncs with V1_LINK_TYPES;
  traverse caps max_depth at 3; list_for_node caps limit at 100;
  combined surface = 7 tools.
* read_relationlink_for_node: binds (workspace_id, entity_id, limit);
  OR on from/to in the WHERE clause; respects validity_state='active';
  caps limit at MAX_LIST_LIMIT=100.
* traverse_relationlink: outgoing one-query / both two-queries;
  max_depth cap; rejects empty link_types; dedupes by min-depth;
  rejects unknown direction.
* create_relationlink: raises RelationLinkWriteNotAvailableError
  with message naming all three substrate-side blockers verbatim
  (operators can grep + see the closure conditions inline).
* Provider-level handlers: create envelope shape, link_type +
  node_kind validation, traverse routing + V1 vocabulary check,
  list_for_node routing + limit cap, provider.handle_tool_call
  iso_link_* prefix dispatch.
* Capability stub fires + logs D-kr3-st1-capability-check-deferred
  on traverse path (proves the shared stub is invoked by both
  families).

Local gates:
* ty check — 7,337 diagnostics, zero-delta vs KR-3 ST1 baseline.
* pytest tests/plugins/memory/ — 294/294 passing (22 new ST2 + 22
  ST1 + 250 pre-ST2).
* Full suite via xdist (-n auto): 24,617 / 142 failed / 129 skipped
  vs KR-2 ST4 merge baseline (24,570/143/129): +47 passed, -1 fail.
  Failures still concentrated in tests/tools/* + tests/skills/* /
  tests/tui_gateway/* xdist isolation noise; none touch isokron.

Rule-6:
* BUILD_DEVIATIONS.md adds D-kr3-st2-no-relationlink-write-mcp-tool
  under Open with all three blockers + closure condition spelled out.
* No README change needed — KR-3 ST3 owns the operator-facing
  tool-surface docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 9a70757 into main May 20, 2026
rafe-walker added a commit that referenced this pull request May 22, 2026
Phase 2 Feature 3 frontend. Pairs with CC#1 KR-FEAT-EMAIL (outbound-only after Option-D descope).

- Backend /api/email/recent stub (4 messages spanning inbound/outbound/filtered/attachment).
- EmailPanel.tsx — stats + filter pills + spoofing chip + HTML-metadata chip + attachment chip + 400-char body preview.
- Dashboard card #10 (Mail icon).

4-layer security contract with email-specific guards: no raw addresses (per-field + walk-payload) + message_id stub shape + plain-text rendering (with dangerouslySetInnerHTML ban) + walk-payload sweep for Purelymail-token-hints / HMAC-secret-shapes / bearer-token-shapes.

Layout: 10 cards = 3+3+3+1 wrapping to 3+3+4 in lg:grid-cols-3.

241/241 admin-panel tests pass across 21 suites; tsc --noEmit + vite build clean.

Flagged pre-existing: 4 tsc -b errors in HeartbeatPanel.tsx + DashboardPage.tsx from HeartbeatStatus enum drift (unknown added) and nullable last_check_at not propagated — confirmed on bare base, NOT introduced by this PR. Recommended cleanup alongside task NousResearch#269.
rafe-walker added a commit that referenced this pull request May 24, 2026
#161)

R3-4 item #10. Per-route cost counter accumulator wired into record_inference. Three windows: process-lifetime, rolling_24h, monthly. RLock concurrency-safe (1000-call/10-thread test verified). Side-channel observer of billing — doesn't mutate cost-ladder accumulation.

Route wired: slack_dm (handlers/slack_dm_handler.py:676).
Routes reserved (literal accepted, consumers wired in follow-on buckets): email_inbound, email_outbound_compose, mcp_tool, alert_investigation, probe_investigation, tool_loop_iteration, scheduled_task.
Follow-ons surfaced: KR-EMAIL-COST-BILL, KR-MCP-TOOL-COST-TAG, KR-REASONING-ITERATION-TAG, KR-PLUGIN-AUDIT-COST-TAG.

Snapshot schema_version 1 → 2; compute_snapshot() now includes cost_telemetry section with rolling_24h + monthly windows. /api/snapshot end-to-end verified.

56 new tests + 141/141 focused regression + ruff clean.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant